Hi! Welcome back! Getting stuck right in, there’s loads to do. I have a week two weeks before the end of the course, after which I’m apparently going to be having a week of interviews with games companies (not sure the details yet), and I want THRUST//DOLL to have a working prototype by then.

  1. Serialising the collision
  2. Putting shaders in your shaders?
  3. Building the SDF in DOTS
    1. Using a blob asset?
    2. Not using a blob asset
  4. Colliding with the SDF
  5. How to get out of walls
  6. Giving it proper animations
    1. Dynamically sampling animations
    2. Getting the LERP parameter
  7. Final thoughts

Dreaming kindly read the last devlog and gave me some really fantastic advice on a few of the mistakes I’ve made. I’m gonna reproduce it here, since it’s really useful information.

  1. I didn’t realize you were trying to make a convex mesh per triangle. I don’t recommend that. The authoring components for colliders are optional, and you can just add a Collider to an entity through a baker yourself. So if you want an entity per triangle, just have a baker that iterates through each triangle, calls CreateAdditionalEntity(), and adds a Collider to that entity in the form of a TriangleCollider. Triangles vs Spheres/Capsules don’t use the Unity GJK path either, which may work a lot better for you (that GJK bug must be some accidental regression because I tested that extensively in 0.5).
  2. For SDFs, there’s an “onion” modifier that works similarly to “Solidify” in Blender and would get you the double-sided effect you are looking for: https://iquilezles.org/articles/distfunctions/
  3. For shaders, the easiest way to make an Entities Graphics compatible shader with something really custom is to use the file mode of a Custom Function Node in Shader Graph. This let’s you specify an include file in shader graph and a function inside it to call. That include file can then include other files and with that you could include the full SDF stack.
  4. There’s no branch prediction in shaders. For tiny branches that involve simple assignments and arithmetic, modern GPU architectures handle these cases more efficiently than CPUs. Your performance problems are much more likely to be special functions like logarithms, exponentials, and trig. Unity has a bunch of “fast” versions of these functions which may be sufficient for your use cases.
  5. Do not subclass BakingSystem. That system is horribly named and is a special system which governs bakers. If you need the BlobAssetStore, you fetch the BakingSystem from your own system. Or you can write a custom Smart Blobber and let the framework handle the BlobAssetStore for you.
  6. You don’t need to hardcode an array size in shaders if you use a proper graphics buffer setup. I can walk you through the steps if you need help with that. Kinemation uses dynamically sized buffers and offsets for all of the skinned mesh rendering, and uses Burst jobs to upload it.
  7. Don’t set global shader values in a baker or baking system. That won’t persist into builds.

That last one especially is a big oof. I think I was assuming that when you build the project, Unity exports all the GameObjects and stuff, and it builds at the beginning of the game. But it sounds like what gets exported is actually in pre-built ECS form. Worth bearing in mind!

In the end I won’t actually be fixing the shader writing issues in this devlog. Instead, this will mostly be about bouncing off the walls…

Serialising the collision

First up, I need to serve Dreaming a serialised collision. He wrote a serializer—it’s a bit long to reproduce the full code here, but it will be in a future version of Latios, and essentially it’s just a file I could drop into the framework. The function signature is…

public static NativeText LogDistanceBetween(in Collider colliderA,
                                            in TransformQvvs transformA,
                                            in Collider colliderB,
                                            in TransformQvvs transformB,
                                            float maxDistance,
                                            Allocator allocator = Allocator.Temp)

and with the note that…

you can construct a TransformQvvs directly from a RigidTransform using the constructor

I wasn’t sure what QVVS stands for. Apparently (per the forum) it’s more performant than matrix-based transforms. Having a look at how it’s defined, it seems to be a quaternion, a position vector, a stretch vector, and a scale. So I guess it stands for Quaternion Vector Vector Scalar?

…Dreaming confirmed this and added:

Correct. Most engines that don’t use matrices use QVV and have weird non-uniform scaling rules. I believe Unreal is like this.

Anyway, let’s set up about using it. I called it like this:

UnityEngine.Debug.Log
    ( Latios.Psyshock
        .PhysicsDebug
        .LogDistanceBetween
            ( result.bodyA.collider
            , new Latios
                .Psyshock
                .PhysicsDebug
                .TransformQvvs
                    ( result.bodyA.transform
                    )
            , result.bodyB.collider
            , new Latios
                .Psyshock
                .PhysicsDebug
                .TransformQvvs
                    ( result.bodyB.transform
                    )
            , 0f
            )
    );

and arranged matters so that there would be an immediate collision on the first frame. The serialiser simply hex encodes the colliders and transforms, producing a long string of letters and numbers. I uploaded these to Dreaming. Curiously, on his side using the 0.7 version of Latios, the results were correct, even though the relevant code paths should not have changed. This is frankly bizarre. The possibility of a Burst compilation problem has been raised.

For now I can get away without convex colliders, so this isn’t an overwhelming problem.

Putting shaders in your shaders?

It would be fantastic if we could get the raymarching shader quad to live comfortably within the land of entities. So let’s see if it’s possible to embed the shader within a shadergraph like Dreaming mentioned.

A quick investigation shows that the Custom Function Node requires a Shader Include File, which has an extension of the form .hlsl or .cginc. What I have right now is a .shader file, which defines various passes each with its own HLSL block. (You can read the template here.) These HLSL blocks consists of a long series of #pragma declarations followed by an #include of some HLSL file. Unfortunately, on some investigation, it looks like the Shader Graph doesn’t support multiple passes.

So why is the shader incompatible? Maybe I can make it SRP batcher compatible? The error I get in the inspector is…

Material property is found in another cbuffer than “UnityPerMaterial” (_DistanceMultipler)

If we can put the material properties in an appropriate CBuffer, would it solve it? But then I started wondering, what is a CBuffer when it’s at home anyway? It seems that it is a constant buffer. How do you declare a CBuffer? It seems that you need to declare stuff between a CBUFFER_START(UnityPerMaterial) and a CBUFFER_END. However, I can’t actually find those lines anywhere in the default URP Lit shader, which is definitely compatible.

On some further investigation I convinced myself (incorrectly!) that the material property _DistanceMultiplier isn’t actually used anywhere in the shader. I commented out this property… and it started complaining about _Loop instead. So it’s not as simple as only that one property being a problem.

I tried declaring the CBuffer in the includes (DepthOnly.hlsl, ForwardLit.hlsl etc.) but hit a problem with multiple declarations. (Also it is necessary to pull uRaymarching out of PackageCache to do this, or else manually copy the includes into the main file). I’m too far out of my depth here, and I’ve got too little time to get up to speed, so I’m going to shelve this one for now.

Building the SDF in DOTS

I have a few hours left before my second-last mentor meeting so I’m going to focus on the highest priority thing which is collisions with walls.

Using a blob asset?

First of all, we need to be able to call the SDF from within DOTS. That means I want the metaball locations. Which means… we need to get them into a blob asset. (Honestly it might be simpler to leave them as entities, but I want to try out smart blobbing.)

The process for creating a smart blobber is described at this page. Once a blob is created, it needs to be stored on a component as a BlobAssetReference. That’s actually a small problem: if I go and delete all the entities representing metaballs, wherever should I store the blob asset reference? The answer is probably… the blackboard entity! Except I’m not sure I can get the blackboard entity during baking. When I tried to make my baking system into a SubSystem, I got errors in the console about the world not being a Latios World.

…ah, but that’s because I should be doing it in the SmartBlobberBakingGroup instead of using WorldSystemFilter to put it in baking? …no, if I put it there, it seems all the metaball entities have already been deleted. I guess we have two systems, one with the WorldSystemFilter, t’other the actual Smart Blobber. Actually we can probably eliminate the former one since it main job is to set up the shader and that ought to be happening somewhere else.

I started writing code for a smart blobber, but I realised I wasn’t sure the right way to have a smart blobber aggregate items from multiple baked entities into one array. Since I’m not even sure why I’m trying to do it this way (hypothetical code simplicity? extremely hypothetical performance advantage?) I decided to abandon this approach and just allow the metaballs to be baked normally and simply strip them of their mesh renderer component. (This is also a good idea because I might want to have them move around or something. Not currently part of the plan, but no sense walling myself off from the possibility, especially since metaballs moving around is a huge advantage of raymarching.)

Not using a blob asset

I really need to stop messing around with irrelevant stuff (I haven’t been getting a lot of sleep, executive function is kind of shot right now) and focus on the really important thing: detecting collisions.

The first step is to give a means to evaluate the SDF. To do that, I need the entities in the ECS, but not rendering. My first instinct was to try to delete all the rendering-related components in the baking system. This caused a whole bunch of errors to spam the console.

The answer actually turned out to be to add the DisableRendering tag component. This isn’t ideal because Unity is still baking and storing a whole bunch of irrelevant rendering related data for each metaball, but it will suffice for now.

Now the metaballs are in ECS. I added a new Radius float to each metaball. (Later we might also use this component to define additional types of metaball, like cylinders!)

So the authoring now looks like:

using Unity.Entities;
using Unity.Rendering;

public class MetaballAuthoring : UnityEngine.MonoBehaviour
{
}

public class MetaballBaker : Baker<MetaballAuthoring>
{
    public override void Bake(MetaballAuthoring authoring)
    {
        float radius =
            ( authoring.transform.localScale.x
            + authoring.transform.localScale.y
            + authoring.transform.localScale.z
            ) / 6f;
        AddComponent
            ( new Metaball
                { Radius = radius
                }
            );
        AddComponent<DisableRendering>();
    }
}

With this change, I can pull MetaballBakingSystem out of the baking system group, since it no longer has to run before the metaballs are deleted. Just have to drop it into `InitializationSystemGroup` LatiosWorldSyncGroup and then call Enabled = false; at the end. That still leaves replacing the SetGlobal calls with the right and proper way to put data into shaders, when I find out what that is.

While I’m at it, I can set _Smooth as a global as well, to make sure it has one single common source (the Level config).

Colliding with the SDF

So, everything that can collide with walls (essentially, players and bullets) will need to evaluate the SDF any frame that it moves. For now I’m only going to implement sphere colliders for the SDF, since they’re trivial in an SDF. (The player should likely have different colliders for bullets and wall bounce/attach anyway!)

I think the way to do this will be to split the calculation into two steps. First, we evaluate the SDF for all the entities that can collide with it, and flag the entities that are close enough to hit a wall. Secondly, we have systems that consume these collisions.

Evaluating the SDF should be pretty cheap, at least in theory. If it isn’t I can look into writing some sort of broadphase. The notion of an AABB becomes a bit complicated with metaballs, but there is probably some way to rule out the metaballs that are miles away.

So, first step is to make a new CollidesWithSDF component with a collision radius, and a corresponding authoring.

Evaluating the SDF is a double iteration. The outer loop will be a chunk iteration over everything with Translation and CollidesWithSDF. The inner loop therefore has to be iteration over the metaballs. The outer loop is processed in parallel by jobs. Is it faster to pass an EntityQuery to a job, or extract an array from that query and pass that instead? Probably to pass the arrays, right, since constructing that array might have some overhead?

(I’m sure it’s obvious that I have no idea what I’m doing with optimisation? I’m just cargo culting ideas like ‘iteration over an array of small things is good for cache hits’ and ‘don’t branch if you can help it’. I can’t properly profile anything until I can build my game outside the editor, and that depends on Unity fixing that Linux linking issue. They say ‘premature optimisation is the root of all evil’ but to be honest this project has been premature optimisation from the minute I said “I should use the ECS”. Any CPU optimisations are completely moot anyway, because until I get the raymarching under control, we’re hard GPU-bound. I hope I’ll get a proper chance to learn the error of my ways when I get an actual job!)

OK, enough blather! Here’s an implementation of these collisions.

using Unity.Entities;
using Unity.Burst;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Collections;
using Latios;

[UpdateInGroup(typeof(LevelSystemGroup))]
[UpdateAfter(typeof(VelocitySystem))]
[BurstCompile]
partial struct SDFCollisionSystem : ISystem
{
    LatiosWorldUnmanaged _latiosWorld;

    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        _latiosWorld = state.GetLatiosWorldUnmanaged();
    }

    [BurstCompile]
    public void OnDestroy(ref SystemState state)
    {

    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        Level level =
            _latiosWorld
                .sceneBlackboardEntity
                .GetComponentData<Level>();

        var MetaballsQuery =
            SystemAPI
                .QueryBuilder()
                .WithAll<Metaball,Translation>()
                .Build();

        NativeArray<Metaball> MetaballRadii =
            MetaballsQuery
                .ToComponentDataArray<Metaball>(Allocator.TempJob);

        NativeArray<Translation> MetaballTranslations =
            MetaballsQuery
                .ToComponentDataArray<Translation>(Allocator.TempJob);

        EntityCommandBuffer.ParallelWriter ecb =
            SystemAPI
                .GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>()
                .CreateCommandBuffer(state.WorldUnmanaged)
                .AsParallelWriter();

        new SDFCollisionJob
            { MetaballRadii = MetaballRadii
            , MetaballTranslations = MetaballTranslations
            , Smooth = level.MetaballSmoothing
            , ECB = ecb
            }
            .ScheduleParallel();
    }
}

partial struct SDFCollisionJob : IJobEntity
{
    [ReadOnly] public NativeArray<Metaball> MetaballRadii;
    [ReadOnly] public NativeArray<Translation> MetaballTranslations;
    public float Smooth;

    EntityCommandBuffer.ParallelWriter ECB;

    void Execute
        ( [ChunkIndexInQuery] int chunkIndex
        , Entity entity
        , in Translation translation
        , in CollidesWithSDF collider
        )
    {
        float expDistance = 0;

        for (int i = 0; i < MetaballRadii.Length; ++i) {
            float metaballRadius = MetaballRadii[i].Radius;
            float3 metaballTranslation = MetaballTranslations[i].Value;

            float sphereDist = math.length(translation.Value - metaballTranslation) - metaballRadius;

            expDistance += math.exp(-sphereDist * Smooth);
        }

        float distance = math.log(expDistance);

        float3 normal = new float3(0f); // todo: calculate normal

        if (distance < collider.Radius) {
            ECB.AddComponent
                ( chunkIndex
                , entity
                , new SDFCollision
                    { Normal = normal
                    }
                );
        }
    }
}

I made what may be a naive error here, in that I’m carrying out a structural change using a ECB.ParallelWriter. Apparently… that’s really slow compared to other means of making a structural change. It would be much faster to use enableable components, basically having a disabled collision on everything that can collide, but that could make iteration slower elsewhere… but really only for iterating over collisions. I had it in my head that I should always use ECBs to avoid introducing the dreaded sync points, but now I’m not so sure.

On the other hand, even when there are hundreds of bullets flying around, probably only a few actually collide with terrain on any given frame? So maybe trying to optimise the structural change is a waste of effort.

On the other other hand, using enableable components would let me apply collision logic on the same frame rather than the subsequent frame.

Worry about that later. Does it work? No idea, we have to actually do something with these collisions. The simplest and most blatant behaviour would be to stop the player dead the very second, nay, the very frame (after) they collide with the SDF.

That’s as simple as…

using Unity.Entities;
using Unity.Burst;
using Unity.Mathematics;

[BurstCompile]
[UpdateInGroup(typeof(LevelSystemGroup))]
[UpdateBefore(typeof(VelocitySystem))]
partial struct PlayerWallCollisionSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {

    }

    [BurstCompile]
    public void OnDestroy(ref SystemState state)
    {

    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        foreach
            ( var (velocity, collision) in
                SystemAPI
                    .Query<RefRW<Velocity>,RefRO<SDFCollision>>()
            )
        {
            velocity.ValueRW.Value = new float3(0f);
        }
    }
}

No jobs because there’s only ever going to be at most one thing in this iteration.

And this… works! You can fly into a wall and get your face stuck. Getting out of the wall is predictably slow since the collisions get registered every frame and zero out your velocity.

How to get out of walls

To do more than just stop dead—for example, to attach limpet-like to the wall with an animation, or bounce off it—we’re going to need the collision normal.

The collision normal can be calculated from a SDF by using finite differences to evaluate the gradient. This is usually used in rendering, but it makes just as much sense to use in collisions.

Using what Inigo Quilez calls the ‘tetrahedron technique’, we can get away with just four evaluations of the SDF. You can read about the mathematical details on his website but the short version is, in GLSL,

vec3 calcNormal( in vec3 & p ) // for function f(p)
{
    const float h = 0.0001; // replace by an appropriate value
    const vec2 k = vec2(1,-1);
    return normalize( k.xyy*f( p + k.xyy*h ) + 
                      k.yyx*f( p + k.yyx*h ) + 
                      k.yxy*f( p + k.yxy*h ) + 
                      k.xxx*f( p + k.xxx*h ) );
}

With this particular SDF, we can accumulate the four SDF evaluations for the normal at the same time as evaluating the main SDF, at the cost of evaluating the normal even in cases where it’s not needed because there’s no collision. That would look like…

partial struct SDFCollisionJob : IJobEntity
{
    [ReadOnly] public NativeArray<Metaball> MetaballRadii;
    [ReadOnly] public NativeArray<Translation> MetaballTranslations;
    public float Smooth;

    public EntityCommandBuffer.ParallelWriter ECB;

    void Execute
        ( [ChunkIndexInQuery] int chunkIndex
        , Entity entity
        , in Translation translation
        , in CollidesWithSDF collider
        )
    {
        float expDistance = 0;
        float4 normalExpDists = new float4 (0);

        float epsilon = 0.0001f;
        float2 k = new float2(1, -1);

        for (int i = 0; i < MetaballRadii.Length; ++i) {
            float metaballRadius =
                MetaballRadii[i].Radius;
            float3 metaballTranslation =
                MetaballTranslations[i].Value;
            float3 pos =
                translation.Value;

            float sphereDist =
                sphereSDF
                    ( pos
                    , metaballTranslation
                    , metaballRadius
                    );

            float4 normalDists =
                new float4
                    ( sphereSDF
                        ( pos + k.xyy * epsilon
                        , metaballTranslation
                        , metaballRadius
                        )
                    , sphereSDF
                        ( pos + k.yyx * epsilon
                        , metaballTranslation
                        , metaballRadius
                        )
                    , sphereSDF
                        ( pos + k.yxy * epsilon
                        , metaballTranslation
                        , metaballRadius
                        )
                    , sphereSDF
                        ( pos + k.xxx * epsilon
                        , metaballTranslation
                        , metaballRadius
                        )
                    );

            expDistance += math.exp(-sphereDist * Smooth);

            normalExpDists += math.exp(- normalDists * Smooth);
        }

        float distance = math.log(expDistance);

        float3 normal =
            math.normalize
                ( k.xyy * normalExpDists.x
                + k.yyx * normalExpDists.y
                + k.yxy * normalExpDists.z
                + k.xxx * normalExpDists.w
                );

        if (distance < collider.Radius) {
            ECB.AddComponent
                ( chunkIndex
                , entity
                , new SDFCollision
                    { Normal = normal
                    }
                );
        } else {
            ECB.RemoveComponent<SDFCollision>
                ( chunkIndex
                , entity);
        }
    }

    float sphereSDF(float3 pos, float3 spherePos, float sphereRadius)
    {
        return math.length(pos - spherePos) - sphereRadius;
    }
}

This evaluates the normal. What to do with it? Well, a starting point might be to reflect the velocity, and align the doll to the new direction of motion.

If the velocity is \(\mathbf{v}\), and the normal is \(\hat{\mathbf{n}}\), the reflected velocity is \(\mathbf{v}-2(\hat{\mathbf{n}}\cdot\mathbf{v})\hat{\mathbf{n}}\). But we also need to align the doll to this.

The simplest way to do this would be to reuse the code we wrote for the thrust system. Some of the animations, particularly the Immelman-type turn, might well work well for kicking off a wall. However, this may play awkwardly with the economy of thrusts, and the thrust sequence code isn’t entirely modular, since if we use those components then the ThrustSequenceSystem will end up playing the whole sequence, extended thrust and all

Still, we can write broadly similar code.

To work out how to process a thrust, we need to store two pieces of information, the initial velocity and the normal.

using Unity.Entities;

partial struct WallKick : IComponentData
{
    public float3 IncidentVelocity;
    public float3 ReflectionVelocity;
    public float3 Normal;
    public quaternion BackRotation;
    public quaternion TargetRotation;
    public double TimeCreated;
}

For convenience, it also seems worth storing the final velocity so we don’t have to recalculate it on every frame of the kick.

Here’s what needs to happen when you kick off a wall:

There are two types of wall interaction animation to cover. For minor angle changes, the doll should simply align her feet towards the wall and kick off. For major angle changes that reverse direction, she should do a tumble-turn manoeuvre.

In the original design document, bouncing off walls is an upgrade represented visually by digitigrade legs. I don’t have time to create an alternative leg model in the prototype. However, I still like the idea of replacing the character’s legs. Since I don’t think stopping on walls is fun, it may be that the upgrade actually adds energy when you bounce.

A saving grace of this compared to the thrust system is that there is only one phase of the animation. So we don’t need to create a complicated chain of animations.

One complication of this design is that without Inverse Kinematics, clipping is all but guaranteed. Implementing IK is too large a project at this stage to include in the prototype, so that will be shelved for a later day.

With those caveats in mind, first we need to add the component that will be used to animate the wallkick. First, to start it:

[WithAll(typeof(Character))]
[WithNone(typeof(WallKick))]
partial struct WallKickStartJob : IJobEntity
{
    public double Time;
    public EntityCommandBuffer ECB;
    public float FlipTransitionIn;
    public ComponentLookup<AnimationTransition> TransitionLookup;

    void Execute
        ( Entity entity
        , ref Velocity velocity
        , in Rotation rotation
        , in SDFCollision collision
        , ref CurrentAnimationClip clip
        )
    {
        // clear any animation transition
        if ( TransitionLookup.TryGetComponent(entity, out var transition) )
        {
            clip =
                new CurrentAnimationClip
                    { Index = transition.NextIndex
                    , Start = transition.Start
                    , Looping = transition.Looping
                    };
        }

        //end any thrust early
        ECB.RemoveComponent<ThrustActive>(entity);
        ECB.RemoveComponent<Thrust>(entity);

        //create a new WallKick
        float3 reflectionVelocity =
            velocity.Value - 2 * math.dot(velocity.Value, collision.Normal) * collision.Normal;

        ECB.AddComponent
            ( entity
            , new WallKick
                { IncidentVelocity = velocity.Value
                , ReflectionVelocity = reflectionVelocity
                , Normal = collision.Normal
                , BackRotation =
                    quaternion
                        .LookRotationSafe
                            ( reflectionVelocity
                            , math.mul(rotation.Value, new float3 (0, -1, 0))
                            )
                , TargetRotation =
                    quaternion
                        .LookRotationSafe
                            ( reflectionVelocity
                            , new float3 (0, 1, 0)
                            )
                }
            );

        //start the wallkick animation - todo make this logic better
        ECB.AddComponent
            ( entity
            , new AnimationTransition
                { NextIndex = AnimationClipIndex.TurnReverse 
                , Start = (float) Time
                , Duration = FlipTransitionIn
                , Looping = false
                }
            );

        //zero out the velocity - make sure to do this after creating the wallkick!
        velocity.Value = new float3(0f);
    }
}

This is scheduled only on one thread since it should only touch at most one entity, so I won’t give it a ParallelWriter, just a normal ECB, which is apparently faster. Later when the time comes for heavy performance optimisation, I can see about trying to do some of the work on the main thread instead (as simple as substituting Run for Schedule). Anyway.

Now we need a new system to handle the rotation. This has identical logic to ThrustAlignmentSystem. In fact, we can actually reuse this code. Since ThrustSequenceSystem only works on something with Thrust, I can rename ThrustFlip to just plain Flip and ThrustRotation to just plain… well Rotation won’t do since that’s taken, but RotateTo perhaps. This fits the ECS spirit of components and systems being small modular pieces of behaviour. I think.

I actually will adjust the ThrustSequenceSystem. Flip doesn’t need to have PreviousDrag, and ThrustRotation shouldn’t have BeforeActive, that was a hack. Instead, ThrustWindup can come back from the dead and store this information. A fourth job can be added to ThrustSequenceSystem which will end the ThrustWindup and apply ThrustActive. Once I’ve done this, ThrustRotationEndJob can actually go back to being a mere StatusEndJob. At the same time as doing this, I can get rid of all those unnecessary ParallelWriters.

(If that makes no sense because you’re not deeply immersed in the world of this project, just suffice to say I cleaned up and simplified the code a bit.)

Then we can move the BackRotation and TargetRotation onto their familiar home in Flip and all the logic from before should handle everything, even putting the RotateTo after the flip.

So with this in mind, WallKickStartJob looks like

[WithAll(typeof(Character))]
[WithNone(typeof(WallKick))]
partial struct WallKickStartJob : IJobEntity
{
    public double Time;
    public EntityCommandBuffer ECB;
    public float FlipTransitionIn;
    public ComponentLookup<AnimationTransition> TransitionLookup;

    void Execute
        ( Entity entity
        , ref Velocity velocity
        , in Rotation rotation
        , in SDFCollision collision
        , ref CurrentAnimationClip clip
        )
    {
        // clear any animation transition
        if ( TransitionLookup.TryGetComponent(entity, out var transition) )
        {
            clip =
                new CurrentAnimationClip
                    { Index = transition.NextIndex
                    , Start = transition.Start
                    , Looping = transition.Looping
                    };
        }

        //end any thrust early
        ECB.RemoveComponent<ThrustActive>(entity);

        //create a new WallKick
        float3 reflectionVelocity =
            velocity.Value - 2 * math.dot(velocity.Value, collision.Normal) * collision.Normal;

        ECB.AddComponent
            ( entity
            , new WallKick
                { IncidentVelocity = velocity.Value
                , ReflectionVelocity = reflectionVelocity
                , Normal = collision.Normal
                }
            );

        // initiate the animation
        ECB.AddComponent
            ( entity
            , new AnimationTransition
                { NextIndex = AnimationClipIndex.TurnReverse 
                , Start = (float) Time
                , Duration = FlipTransitionIn
                , Looping = false
                }
            );

        // rotate the character to point along the reflected velocity vector
        ECB.AddComponent
            ( entity
            , new Flip
                { InitialRotation =
                    rotation.Value
                , BackRotation =
                    quaternion
                        .LookRotationSafe
                            ( reflectionVelocity
                            , math.mul(rotation.Value, new float3 (0, -1, 0))
                            )
                , TargetRotation =
                    quaternion
                        .LookRotationSafe
                            ( reflectionVelocity
                            , new float3 (0, 1, 0)
                            )
                , TimeCreated =
                    Time
                }
            );

        //zero out the velocity
        velocity.Value = new float3(0f);
    }
}

All that remains is to reapply the reflected velocity at the appropriate point in the animation. Hooray!

Since it’s convenient to handle all the sequencing logic in one place, I think I will rename ThrustSequenceSystem to SequenceSystem. Possibly it could even merge with StatusTransitionSystem at some point. Now, WallkickEndJob is simple:

partial struct WallkickEndJob : IJobEntity
{
    public double Time;
    public double WallkickStopDuration;
    public float TransitionDuration;

    public EntityCommandBuffer ECB;

    void Execute
        ( in WallKick wallkick
        , ref Velocity velocity
        , Entity entity
        )
    {
        if (((float) (Time - wallkick.TimeCreated)) > WallkickStopDuration)
        {
            velocity.Value = wallkick.ReflectionVelocity;
            ECB.RemoveComponent<WallKick>(entity);
            ECB.AddComponent
                ( entity
                , new AnimationTransition
                    { NextIndex = AnimationClipIndex.LevelFlight
                    , Start = (float) Time
                    , Duration = TransitionDuration
                    , Looping = true
                    }
                );
        }
    }
}

The only problem with this is… we will immediately register a collision with the wall again after this and get stuck. (The direction coming off the wall is also quite unexpected, but I’ll solve that next.)

So we need something of a grace period to clear the wall. Rather than set this to a hardcoded time, I’ll add a component that prevents running WallKickStartJob but gets removed as soon as no collision is registered. It’s possible this could lead to some clever exploits where you could find a way to bounce out of the level by staying in a narrow space where you preserve your collision state after bouncing. But for a prototype system it seems like a reasonable start. Also, you wouldn’t be able to thrust while outside of the level and the SDF filling the screen would make navigation almost impossible, so rather than being a speedrun exploit, this seems more like a danger of happening by accident. Anyway.

[WithAll(typeof(Character), typeof(ClearingWall))]
[WithNone(typeof(WallKick), typeof(SDFCollision))]
partial struct ClearingWallJob : IJobEntity
{
    public EntityCommandBuffer ECB;

    void Execute
        ( Entity entity
        )
    {
        ECB.RemoveComponent<ClearingWall>(entity);
    }
}

I gave this a test run… and it works pretty good actually! Here’s a video of what happens…

Giving it proper animations

The actual motion feels pretty good with a 0.1s stop, but the animation definitely lags it too much to be effective. The problem is that I can’t start the animation until I’ve already collided with the wall. The solution may be to actually have two colliders, an outer collider that initiates the anticipation animation and an inner collider that triggers the stop and turn. Of course, this means sometimes the outer collider will trigger but the inner collider won’t… 仕方がない! In that case, we can just transition back into the level flight animation as soon as we no longer register collisions with the outer.

Once it’s fully complete the ‘turn’ animation family will have nine different animations. This is excessive for wall kicks. I think, with clever use of rotation, I might be able to get away with just two different animations: one for shallow bounces, and one for large bounces.

So, let’s go in Blender and see about animating these possibilities. Here’s the idea for the shallow bounce:

If that’s not clear, here’s an animation (root motion only) in Blender:

Rolling should ideally have some anticipation, and in fact there might be a clever way to make this procedural with only one animation between two extreme poses. But let’s get the bounce animation working first.

Basically, this animation will only take effect when the outer collider is red. This will trigger the root motion (rolling) and start playing the animation. If the inner collider gets triggered we rapidly jump to a second animation. We get a little leeway afterwards during which the animation will be in the process of blending back to level flight.

With this in mind, I animated thus…

You squish into the wall and push off it. Theoretically this should work with any shallow collision.

Now, this is just one animation. It won’t make much sense if you never touch the wall. My first thought was to create an animation with less squish. This is the same up to when the previous animation would have hit the inner collider, but from there, I’ll relax back to the rest pose.

However, I had trouble coming up with a natural-feeling animation for this. And there’s a more fundamental problem, to do with animation timing.

Suppose we register a collision with the outer collider. It might take a short time or a long time to also hit the inner collider, depending on the speed relative to the surface. So we can’t simply play the animation at a fixed speed.

HBut, since we have full control over animation sampling, we’re not limited to playing an animation at a fixed speed. In fact, we can use any parameter we please to sample the animation. Including… distance. Which is served up by the Signed Distance Field.

So instead of having two colliders, I can dynamically scrub through the animation. I love this idea, so I’m going to do it.

However, there’s a problem with this approach. The animation is not symmetric in distance, so it would look very unnatural to use distance directly. In short this is a situation with hysteresis.

This is a solvable problem, with a bit of finagling. We could find the delta between frames, and add up the absolute value, to essentially compute \(\int \left\| \df{\text{SDF}}{t} \right\|\dif t\). This would work great if we could guarantee that \(\df{\text{SDF}}{t}\) remained almost always monotonically increasing, but there is a period of \(0.1\unit{s}\) in which the character is completely stationary, and yet it’s crucial that the animation continue in this time. So, we need a different solution for evaluating this section of the animation parameter function. Of course, in this section, time can simply be increased linearly. This may lead to an unnatural-seeming jerk where the animation suddenly gets faster. If this is a problem I can treat it as two different animations and blend it.

With this in mind it may be a good idea to have two colliders after all - or rather, two notional colliders, and rather than evaluate the SDF twice we can store the value.

So here’s how it’s going to work:

…writing this devlog might seem like a lot of work, but in cases like this, planning out exactly what I’m going to do before I try to write it is a huge help. Let’s go.

Dynamically sampling animations

What is the right way to pass the time override to the jobs that handle sampling? Sampling is heavy so these jobs should run as fast as possible. That said, there is only one animated character to sample right now, so a little overhead isn’t so bad. Still, when the time comes to spawn enemies, we might want to rig them too. Or random stuff in the environment. But then, maybe we’ll want to override their animation too?

For simplicity’s sake I’m going to add a little random access componentlookup. Make sure to declare it [ReadOnly] (which is in Unity.Collections) or Unity will shout at you.

float clipTime;
        
if (OverrideLookup.TryGetComponent(entity, out AnimationClipTimeOverride timeOverride))
{
    clipTime = timeOverride.ClipTime;
} else {
    clipTime =
        currentAnimation.Looping
            ? clip.LoopToClipTime(Time - currentAnimation.Start)
            : Time - currentAnimation.Start;
}

For transitions, it’s the same, but I apply the override time to the incoming transition clip, not the outgoing clip (i.e. the override sets nextClipTime). I’m sure I’ll have to cover the other case at some point but not worth worrying about now.

Getting the LERP parameter

The distance I write on SDFCollision is the value of the Signed Distance Field at the centre of the doll. A collision is registered when this is less than her CollidesWithSDF radius. She will have a second, inner radius which will also be stored on CollidesWithSDF (in case this logic is ever needed elsewhere.)

If the outer radius is \(R\) and the inner radius is \(r\), and the value of the SDF is \(s\), then the LERP parameter \(\tau\) is computed…

\[\tau = 1 - \frac{s - r}{R - r}\]

How to lerp to the correct orientation? It’s probably easiest to do it in the same job we calculated tau. We can set a RotateTo instead of a Flip when the wall comes into range, and constantly update its TargetRotation value as we move. This would mean we’d have to add logic to override RotateTo. Seems better to store the InitialRotation on something, maybe FaceWall, and do the interpolation right here. FaceWall also serves as a tag component so this job only kicks in when it’s applied.

After that all that remains is setting up the animation clip time override. This is actually a little tricky because it depends on the duration of the relevant segment of the animation clip. So that has to be passed as a struct field, which I’ll call NotionalDuration. That’s one more for Level! (At some point it may become necessary to read all these parameters from some sort of config file, and creating some sort of dynamic hashmap to read them from once and cache them, rather than constantly stuffing things onto the level struct which requires modifying three different files.)

So here’s the ‘every frame’ part of the job.

partial struct WallKickLerpJob : IJobEntity
{
    public float NotionalDuration;

    void Execute
        ( in SDFCollision collision
        , in CollidesWithSDF collider
        , in FaceWall faceWall
        , ref Velocity velocity
        , ref Rotation rotation
        , ref AnimationClipTimeOverride timeOverride
        )
    {
        float tau =
            1f
            - (collision.Distance - collider.InnerRadius)
            / (   collider.Radius - collider.InnerRadius);

        quaternion targetRotation =
            quaternion
                .LookRotationSafe
                    ( velocity.Value
                    , collision.Normal
                    );

        rotation.Value =
            math
                .slerp
                    ( faceWall.InitialRotation
                    , targetRotation
                    , tau
                    );

        timeOverride.ClipTime =
            tau * NotionalDuration;
    }
}

It also needs to clean up at the appropriate moment, i.e. when \(tau>0\). So drop an ECB and the time in there and…

if (tau > 1f) {
    float NotionalStartTime = (float) (Time - NotionalDuration);

    clip.Start = NotionalStartTime;

    float3 targetVelocity =
        velocity.Value
            - 2
            * math.dot(velocity.Value, collision.Normal)
            * collision.Normal;

    quaternion targetRotation =
        quaternion
            .LookRotationSafe
                ( targetVelocity
                , collision.Normal
                );

    ECB.RemoveComponent<FaceWall>(entity);
    ECB.RemoveComponent<AnimationClipTimeOverride>(entity);
    ECB.AddComponent
        ( entity
        , new RotateTo
            { InitialRotation = rotation.Value
            , TargetRotation = targetRotation
            , TimeCreated = Time
            }
        );
    ECB.AddComponent
        ( entity
        , new WallKick
            { InitialVelocity = velocity.Value
            , TargetVelocity = targetVelocity
            , Normal = collision.Normal
            , TimeCreated = Time
            }
        );

    velocity.Value = new float3(0f);
}

The logic on WallKick is actually exactly what’s needed right now. Which is to say it waits for the designated time and then deletes itself.

That just leads WallKickStartSystem. We no longer want to invoke a tumble turn style flip on every wallbounce. (We might not need it at all.) Instead, when we detect a collision at the outer radius, we want to set up this new system…

// initiate the animation
ECB.AddComponent
    ( entity
    , new AnimationTransition
        { NextIndex = AnimationClipIndex.WallKickShallow
        , Start = (float) Time
        , Duration = FacingDuration //controls amount of lerp
        , Looping = false
        }
    );

ECB.AddComponent
    ( entity
    , new AnimationClipTimeOverride
        { ClipTime = 0f
        }
    );

ECB.AddComponent
    ( entity
    , new FaceWall
        { InitialRotation = rotation.Value
        }
    );

And if we never hit the wall, it’s important to clean up when we get out of range:

[WithAll(typeof(AnimationClipTimeOverride))]
[WithNone(typeof(SDFCollision))]
partial struct WallKickCancelJob : IJobEntity
{
    public double Time;

    public EntityCommandBuffer ECB;

    void Execute(Entity entity, in Rotation rotation, in FaceWall faceWall)
    {
        ECB.RemoveComponent<FaceWall>(entity);
        ECB.RemoveComponent<AnimationClipTimeOverride>(entity);
        ECB.AddComponent<RotateTo>
            ( entity
            , new RotateTo
                { InitialRotation = rotation.Value
                , TargetRotation = faceWall.InitialRotation
                , TimeCreated = Time
            );
    }
}

(I forgot to actually run the job that removes the WallKick component here but this will be addressed later.)

With all this done I can now… ricochet madly off walls! It’s great!

There’s a small bug here, where the rotation doesn’t seem to return to the default ‘up’ vector. This is because I forgot to add an instruction to roll back in WallkickEndJob. In some ways that’s neat, but it isn’t intended. I’m feeling unconvinced about the idle animation here—it looked OK when flying through the void but now it’s feeling a bit weird to have her holding her arm up like that. (Of course, it is supposed to be using IK to follow the camera.)

Final thoughts

That seems a good place to wrap up this page of devlog.

As far as the issues Dreaming raised, the shader writing issue is not fixed. Being able to dynamically change the metaball array would be very useful since I could cull any metaballs that aren’t near enough to be relevant. (Also animations.) But I’m not sure I have time to deal with that.

At some point soon it will be necessary to actually do some work on the visuals. That is, design proper shaders for the character and environment. But first, it isn’t a game until the bullets are there, so that is absolute highest priority.

It’s going to be a really frantic week! See you on the other side!!

Comments

Add a comment
[?]