Spite

You are a holy crusader brought back from the dead to cleanse the town from evil cultists.

Level Editor

Did you know, file extensions are made up? We use Unity as our level editor, for our in-house game engine, and it has great ease to write new files using C# BinaryWriter. By collecting the objects in the scene we wish to export to the game we can write bytes of information to a file.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

We open that same file and read bytes in the same order and magnitude in our engine.

Finally on level load, we use the stored data to create and add instances to our game scene. The game scene follows the Singleton design pattern for ease of access.

Once the finicky part, of writing and reading bytes in the same order and magnitude, is hidden behind a neat interface - this solution can truly shine. It tremendously increased iteration speed by allowing everyone in the project to create testing gyms for their assets. Also, by documenting the process of exporting new types of instances to the level, all the programmers had the capacity to do so. This allowed for rapid iterations of design which was proven key for success in short term projects as ours. Finally the level files becomes ridiculously small, up to about 10 kilobytes, which I find satisfying!

       

        string fileName = aFolder + "/ParticleInstances.particleInstances";

        using (System.IO.BinaryWriter file = new System.IO.BinaryWriter(File.Open(fileName, FileMode.Create)))
        {
            foreach (ParticleSystem ps in someParticleSystems)
            {
                ParticleSystemInstance instance = GetParticleSystemInstanceData(ps);
                file.Write(instance.ParticleSystemName.Length);
                file.Write(instance.ParticleSystemName.ToCharArray());

                file.Write(instance.Position.x);
                file.Write(instance.Position.y);
                file.Write(instance.Position.z);

                file.Write(instance.QuaternionRotation.x);
                file.Write(instance.QuaternionRotation.y);
                file.Write(instance.QuaternionRotation.z);
                file.Write(instance.QuaternionRotation.w);

                file.Write(instance.InstanceID);
            }
        }

   

Level::ParticleSystemInstanceData    Level::ExtractParticleSystemInstanceData(std::ifstream& aFile)
{
    Level::ParticleSystemInstanceData data;
    data.myParticleSystemName = FileExtraction::ExtractString(aFile);
    data.myPosition = FileExtraction::ExtractPosition(aFile);
    data.myRotation = FileExtraction::ConvertVector4ToMatrix3f(FileExtraction::ExtractVector4(aFile));
    data.myInstanceID = FileExtraction::ExtractInt(aFile);
    return data;
}

std::string FileExtraction::ExtractString(std::ifstream& aFile)
{
    int stringLength = 0;
    aFile.read(reinterpret_cast<char*>(&stringLength), sizeof(int));
    char* characterArr = new char[stringLength + 1];
    aFile.read(characterArr, stringLength);
    characterArr[stringLength] = '\0';
    std::string output(characterArr);
    delete[] characterArr;
    return output;
}

Vector4f FileExtraction::ExtractVector4(std::ifstream& aFile)
{
    Vector4f vector4;
    aFile.read(reinterpret_cast<char*>(&vector4.x), sizeof(float));
    aFile.read(reinterpret_cast<char*>(&vector4.y), sizeof(float));
    aFile.read(reinterpret_cast<char*>(&vector4.z), sizeof(float));
    aFile.read(reinterpret_cast<char*>(&vector4.w), sizeof(float));
    return vector4;
}

   

    for (auto& i : myParticleSystemInstances)
    {
        CParticleEmitter* emitter = new CParticleEmitter();
        emitter->SetPosition(i.myPosition);
        emitter->SetRotation(i.myRotation);
        emitter->Init(CParticleFactory::GetInstance()->GetParticle(i.myParticleSystemName));
        CScene::GetInstance()->AddParticleEmitter(emitter, i.myInstanceID);
    }

Particle System

For Spite, the technical artists in our project group requested advanced particle systems. Previously, the properties of particle systems were edited through .json files. Now, however, they were to be edited in Unity as particle editor and with many more features.

 

Our particles uses a geometry shader to make quads out of points which we sample the particle texture on.

 

The particle instances consists of a position, a tint, a size modifier and its texture.

 

To determine these properties, each instance is updated through an emitter.

 

The properties of the emitter is determined by its compositioned CParticleSystem which is imported through the level editor. Similarly to the level export, the bytes of information has to be written and read in the same order and magnitude.

I found a new appreciation for math working with particles, with all this data from the editor it became a tall order to replicate the particle system in our engine. 

 

#include "ParticleShaderStructs.hlsli"

[maxvertexcount(4)]
void main(point VertexToGeometry input[1], inout TriangleStream<GeometryToPixel> output)
{
    const float2 offset[4] = {{-1.0f,  1.0f },{ 1.0f,  1.0f },{-1.0f, -1.0f },{ 1.0f, -1.0f }};
    const float2 uv[4] = {{ 0.f, 0.f },{ 1.f, 0.f },{ 0.f, 1.f },{ 1.f, 1.f }};

    float rotation = -input[0].myPosition.w;
    float3 positionInput = input[0].myPosition.xyz;

    GeometryToPixel vertex;
    for (unsigned int index = 0; index < 4; index++)
    {

        float2 offsetVertex = offset[index];

        float x = offsetVertex.x;
        float y = offsetVertex.y;

        offsetVertex.x = (x * cos(rotation) - y * sin(rotation));
        offsetVertex.y = (x * sin(rotation) + y * cos(rotation));

        float4 position = float4(positionInput, 1.f);

        position.xy += offsetVertex * input[0].mySize;

        vertex.myPosition = mul(toProjection, position);
        vertex.myColor = input[0].myColor;
        vertex.myUV = uv[index];

        output.Append(vertex);
    }
}

 

struct VertexInput
{
    float4 myWorldPosition        : POSITION;
    float4 myColor                : COLOR;
    float2 mySize                : SIZE;
};
texture2D ParticleTexture : register(t0);

    class CParticleEmitter : public CGridObject
    {
    public:
        CParticleEmitter() = default;
        ~CParticleEmitter() = default;

        void Init(CParticleSystem* aParticle);
        void Update(float aDeltaTime);
        void Emit(float aDeltaTime);

    private:
        void SpawnParticlesOnInterval(const CParticleSystem::SData::SEmitterData& someData);
        void SpawnParticle(const CParticleSystem::SData::SEmitterData& someData);

        void UpdateLifetimeAndRemoveDeadParticles(float aDeltaTime);
        void UpdateParticles(float aDeltaTime, const CParticleSystem::SData::SEmitterData& someData, const Vector3f& aCameraPosition);

    private:
        std::vector<CParticleSystem::SParticleInstance> myParticleInstances;

        CParticleSystem* mySystem;
        float myEmitTimer;
    };

    for (auto& particle : myParticleInstances)
    {
        float percentageLived = (particle.myLifetimeCurrent / particle.myLifetimeMax);

        float alpha = Keyframe::GetKeyframeValue(someData.myAlpha, percentageLived);

        float size = Keyframe::GetKeyframeValue(someData.mySize, percentageLived);

        Vector3f color = Keyframe::GetKeyframeValue(someData.myColor, percentageLived);

        particle.myTint = Vector4f(color, alpha);
        particle.mySize = VECTOR2F_ONE * size;

        Vector4f direction = GetDirection(particle, aDeltaTime, someData.myGravity);
        float speed = (someData.mySpeed[0] + (someData.mySpeed[1] - someData.mySpeed[0]) * percentageLived);
        particle.myMovement = (direction * speed * aDeltaTime);
        particle.myLocalPosition += particle.myMovement;

        if (someData.myLocalFlag)
        {
            particle.myPosition = particle.myLocalPosition + myTransform.GetPosition() + particle.myStartingWorldPosition;
        }
        else
        {
            particle.myPosition = particle.myLocalPosition + particle.myStartingWorldPosition;
        }

        particle.myRotation += particle.myRotationSpeed * aDeltaTime;
        particle.myDistanceToCameraSqr = (particle.myPosition - aCameraPosition).LengthSqr();
        particle.myPosition.w = particle.myRotation;
    }

   

    using (System.IO.BinaryWriter file = new System.IO.BinaryWriter(File.Open(destinationPath, FileMode.Create)))
    {
        ParticleSystemData particleSystemData = GetParticleSystemData(aParticleSystem);
        aFile.Write(particleSystemData.maxNumberOfParticles);
        aFile.Write(particleSystemData.blendMode);
        aFile.Write(particleSystemData.texturePath.Length);
        aFile.Write(particleSystemData.texturePath.ToCharArray());
        aFile.Write(particleSystemData.coneFlag);
        aFile.Write(particleSystemData.radius);
        aFile.Write(particleSystemData.angle);
        aFile.Write(particleSystemData.spawnInterval);
        aFile.Write(particleSystemData.inheritVelocity);
        aFile.Write(particleSystemData.localToEmitter);
        aFile.Write(particleSystemData.gravityConstant);
        aFile.Write(particleSystemData.lifeDuration.Min);
        aFile.Write(particleSystemData.lifeDuration.Max);
        aFile.Write(particleSystemData.speed.Min);
        aFile.Write(particleSystemData.speed.Max);


        aFile.Write(particleSystemData.sizeOverLifetime.Length);
        for (int i = 0; i < particleSystemData.sizeOverLifetime.Length; i++)
        {
            aFile.Write(particleSystemData.sizeOverLifetime[i].time);
            aFile.Write(particleSystemData.sizeOverLifetime[i].value);
        }
        aFile.Write(particleSystemData.colorOverLifetime.Length);
        for (int i = 0; i < particleSystemData.colorOverLifetime.Length; i++)
        {
            aFile.Write(particleSystemData.colorOverLifetime[i].time);
            aFile.Write(particleSystemData.colorOverLifetime[i].color.r);
            aFile.Write(particleSystemData.colorOverLifetime[i].color.g);
            aFile.Write(particleSystemData.colorOverLifetime[i].color.b);
        }
        aFile.Write(particleSystemData.alphaOverLifetime.Length);
        for (int i = 0; i < particleSystemData.alphaOverLifetime.Length; i++)
        {
            aFile.Write(particleSystemData.alphaOverLifetime[i].time);
            aFile.Write(particleSystemData.alphaOverLifetime[i].alpha);
        }
    
        aFile.Write(particleSystemData.startRotation.Min);
        aFile.Write(particleSystemData.startRotation.Max);
        aFile.Write(particleSystemData.rotationSpeed.Min);
        aFile.Write(particleSystemData.rotationSpeed.Max);
    }

 

    SParticleSystemData CParticleFactory::GetParticleSystemData(const std::string& aParticleName) const
    {
        std::ifstream infile("ParticleSystems/" + aParticleName + ".particle", std::ios::binary | std::ios::in);


        int maxNumberOfParticles = FileExtraction::ExtractInt(infile);
        int blendMode = FileExtraction::ExtractInt(infile);
        std::string texturePath = FileExtraction::ExtractString(infile);

        bool coneFlag = FileExtraction::ExtractBool(infile);
        float radius = FileExtraction::ExtractFloat(infile);
        float angle = FileExtraction::ExtractFloat(infile);

        float spawnInterval = FileExtraction::ExtractFloat(infile);
        bool inheritVelocity = FileExtraction::ExtractBool(infile);
        bool localToEmitter = FileExtraction::ExtractBool(infile);
        float gravityConstant = FileExtraction::ExtractFloat(infile);

        float lifeDurationMin = FileExtraction::ExtractFloat(infile);
        float lifeDurationMax = FileExtraction::ExtractFloat(infile);

        float speedMin = FileExtraction::ExtractFloat(infile);
        float speedMax = FileExtraction::ExtractFloat(infile);

        std::map<float, float> sizeOverLifetime = FileExtraction::ExtractMapOfFloats(infile);
        std::map<float, Vector3f> colorOverLifetime = FileExtraction::ExtractMapOfVector3(infile);
        std::map<float, float> alphaOverLifetime = FileExtraction::ExtractMapOfFloats(infile);

        float startRotationMin = FileExtraction::ExtractFloat(infile);
        float startRotationMax = FileExtraction::ExtractFloat(infile);

        float rotationSpeedMin = FileExtraction::ExtractFloat(infile);
        float rotationSpeedMax = FileExtraction::ExtractFloat(infile);

       {...}
    }

In Particle Editor (Unity)

In-Game

The result is lacking in its randomization and primarily depth sorting. However, the tool served us well in Spite and I'm pleased by the result. I also learned a valuable lesson about using an editor to export presets and properties: Make clear for the user what is possible to export and not. Unity's particle editor is powerful, and the technical artists who used the export tool often misconstrued it, thinking that every parameter in-editor would work in-game. I found that these limits are important to set early, and to communicate them rigorously - That way design work won't be made for naught.