Register

Update #3

Ben
6 months, 1 week ago
Over the past few weeks, I've been slowly going down my list of things to fix and improve for Nirion. One of the bigger tasks was to create a more generic and easy to use particle system solution. This ended up taking a week or two of my time, but so far it seems like it was worth it. Another big focus was on figuring out more interesting puzzle elements and improving level design overall. I believe the last few weeks of development have really gone a long way in making Nirion feel more like a complete game!

Generic Particle System

Up until I reworked this system, all particle effects had unique code written for their setup and simulation. This works great for some more complex effects, like the player evaporating into pixels when saving:



But for simple effects, I found it to be quite a lot of setup and code just to do what is basically changing parameters. My solution to this was to just change common settings in the editor. This is pretty much the solution you would find in something like Unity or Unreal 4. While doing this though, I needed to keep the ability to define effects in code since some effects are just easier to program directly.

My approach was to define a bunch of parameters that the editor exposes, based on the things I think I need to be able to change(velocity, colour, rotation, parallax), and apply these parameters to a "generic" effect type. This effect will just see what parameters are enabled and how, and then simulate the particles based on this.

I ended up with a data structure like this one:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct GenericParticleParameters
{
        GenericParticleParameterF32 lifetime;
        GenericParticleParameterF32 scale;
        GenericParticleParameterF32 velocityX;
        GenericParticleParameterF32 velocityY;
        GenericParticleParameterColour rgb;
        GenericParticleParameterF32 colourA;
        GenericParticleParameterF32 parallax;
        GenericParticleParameterF32 rotationalVelocity;
        GenericParticleParameterF32 animationTime;
};


Where GenericParticleParameterF32 looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
enum GenericParticleParameterType
{
        GPP_Off,
        GPP_Constant,
        GPP_LinearOverLife,
        GPP_CurveOverLife,
        GPP_RandomBetween,
        GPP_RandomBetweenCurvesOverLife,
};
struct GenericParticleParameterF32
{
        GenericParticleParameterType type;
        f32 valueA;
        f32 valueB;
        AnimationCurve curveA;
        AnimationCurve curveB;
};


While editing the effect, the user can select the type from a drop down list and fill in the needed data based on the type. GPP_Off just ignores the parameter, GPP_Constant just sets the value to "valueA" on spawn, GPP_LinearOverLife will perform a linear interpolation between "valueA" and "valueB" over it's total lifetime, GPP_RandomBetween will pick a random value between "valueA" and "valueB" on spawn, GPP_CurveOverLife will evaluate a curve defined in the editor where the x axis is the particle's life, and GPP_RandomBetweenCurvesOverLife will interpolate between two different curves based on a random value that is set on spawn. These settings seem to cover most things I need right now, but it should be pretty easy to add more later if needed. GenericParticleParameterColour is basically just a v3 version of this struct.

Using these parameters when spawning looks like this:

1
2
3
4
5
6
7
8
    if(IsGenericParticleParameterEnabledForSpawn(&genericParams->velocityX))
    {
        EffectModule_GenericF32Parameter(system, system->particles.velocityX, spawnStart, spawnEnd, genericParams->velocityX, randomState);
    }
    if(IsGenericParticleParameterEnabledForSpawn(&genericParams->velocityY))
    {
        EffectModule_GenericF32Parameter(system, system->particles.velocityY, spawnStart, spawnEnd, genericParams->velocityY, randomState);
    }


And during actual simulation update:

1
2
3
4
5
6
7
8
    if(IsGenericParticleParameterEnabledForUpdate(&genericParams->velocityX))
    {
        EffectModule_GenericF32Parameter(system, system->particles.velocityX, 0, system->activeParticles, genericParams->velocityX, randomState);
    }
    if(IsGenericParticleParameterEnabledForUpdate(&genericParams->velocityY))
    {
        EffectModule_GenericF32Parameter(system, system->particles.velocityY, 0, system->activeParticles, genericParams->velocityY, randomState);
    }


EffectModule_GenericF32Parameter will loop over all particles and apply the parameter based on it's type.

Here's what the editor looks like for a generic particle effect:



Each parameter is listed in the entity properties. It's type can be changed through a drop down menu, and it will only show the parameters that apply to the current type. Multiple emitters can be added to the same entity in order to compose interesting effects.

Here are some effects I've done so far:





Obtain Upgrade Sequence

I've finally gotten around to making something happen once you pick up an upgrade. This is definitely still a work in progress, but shows roughly what should happen when an upgrade is picked up:



It's pretty straightforward, the player touches the upgrade, an effect plays and a popup appears telling the player how to use it or what it does. For this I've draw some animated tiles and a dialogue box which is probably way too obviously inspired by early Final Fantasy games. Since this is the first time I've had to add player facing text, I should be using a string table for localisation, but of course I'm putting that off for now.

Editor Improvements

Although the generic particle system itself was pretty straightforward, improving the editor UI to support the various things needed by it took a lot of work.

One early improvement I made was to the entity properties window in general. It use to display properties as one giant tree which would quickly indent very far to the right. I've added the ability to assign properties to categories, and made category headers large and obvious:


I added a basic curve editor which manipulates an array of points with a certain scale. This is pretty basic at the moment and only connects the points together with a straight line. I'm planning on implementing saving/loading for curves, but haven't gotten around to it yet.


I've also implemented a colour picker for easily specifying colours. Until now I've just been typing in RGB manually:


The editor is now a lot nicer to use, but still has a long way to go. I am really spending a minimum amount of time on it, since I prefer to spend time on the game where I can, but it was good to make some large improvements like this over the course of a week or so. All the editor UI is still being done using the IMGUI code I wrote near the start of the project(over a year ago now) and it's still holding up pretty well. I was feeling like it was too verbose for what I needed, but after using it for some of these things I think it works well.

Entity Templates

Up until this point entity types could only be defined in code, as I described in my first blog post. This worked completely fine for the most part, if I wanted to make a new type of entity, I could just define it in code, expose whatever properties it needed, and then change properties on the map instance.

The way entity instances work is by saving their differences with the "default" entity for that type. For example, if I define a point light entity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
TYPE()
 struct PointLightEntity
 {
     PROPERTY()
         f32 radius;
     PROPERTY()
         f32 intensity;  
     PROPERTY()
         v4 colour;
 };
ENTITY_DEFAULT_FUNCTION(EntityDefault_PointLight)
{
    ENTITY_EDITOR_NAME("Point Light");
    entity->pointLight.radius = 20.0f;
    entity->pointLight.intensity = 2.3f;
    entity->pointLight.colour = V4(0.8862f, 0.7254f, 0.4549f, 1.0f);
    
    entity->pauseLevel = SimulationLevel_Cutscene;
    
    PushEntityVisualPlaceholder(system, entity, {});
    
    SET_EDITOR_ICON(BID_EditorPointLightIcon);
    return CreateEntityDefault(entity, true, true);
}
DEFINE_ENTITY(hash="PointLight", type=ET_PointLight, func_Default=EntityDefault_PointLight);


The "default" instance of this point light entity will be constructed by running this function when the game starts up. This is used as a base for saving or loading any point light entities in the future. When we place a point light in the map and change it's radius for example, all that will be saved about that point light entity is that it is a point light and that we've changed the radius. If we want to spawn our point light, we simply need to copy the default point light and then apply our property differences on top of this.

Now that I'm making different types of generic particle effects, which are the exact same type as far as the code is concerned, this approach means that it's not actually possible to spawn these effects since they were created in the editor and there's no way to reference them. My approach to dealing with this problem was to save the entity as a "template" in the same way I would save entities to the map. We save which type the entity is and it's differences from the default of this type. This information is saved as an asset and can be referenced through the asset system just like any other asset type.

When spawning an entity we can now optionally take either a reference to a entity type that was defined in code, or an asset reference to an entity template.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
struct EntityDefaultRef
{
    EntityDefaultRefType type;

    u32 templateHash;
    i32 codeType;
};
static EntityDefaultRef CodeRef(EntityType type)
{
    EntityDefaultRef result = {};
    result.type = EntityDefaultRefType_Code;
    result.codeType = type;
    return result;
}
static EntityDefaultRef TemplateRef(u32 hash)
{
    EntityDefaultRef result = {};
    result.type = EntityDefaultRefType_Template;
    result.templateHash = hash;
    return result;
}


After an an entity template instance is placed in the map, it loses it's reference to the template and acts as if it's just an instance of the underlying code type. I've thought about linking instances to their template type and updating instances whenever the template changes, but it didn't seem necessary to me and seemed to complicate things quite a bit. Overall this works pretty well for referencing editor created entity types.

Level Design

I've also been trying to improve the level design. I've added more objects to interact with and some areas that take more thought to get through. As I've said before, I don't want puzzles to be the main focus of this game, but I think some of the areas I've made so far were a little too empty. Playing through it, this seems like a massive improvement and like it's heading in the right direction.





Conclusion

That's everything I've been working on lately. I'm still going through the mines area and improving the level design mainly, but besides that it's mostly just bug fixes and minor improvements left before I would want to release a demo. Once I get the first area of the game very solid and to a level I'm happy with, hopefully it will be relatively easy to add more and complete the rest of the game. Thanks for reading!
Simon Anciaux
6 months, 1 week ago
Thanks for the post.
The train tracks seem upside down in some screen shots. I mean the "light highlight" isn't consistent.
Ben
6 months, 1 week ago
Yeah you're right. That's on my list of things to fix. I've only draw a couple of tiles and have just been rotating them, but I will need to have unique tiles for different rotations.
Log in to comment