NodeLink2 for moving platforms

Hi there and thank you for your time.

I’m currently working on a 3D platformer with a recast graph, and I’m trying to add moving platforms for the player and the AI to use. I’m using FollowerEntity, and the platforms are simply moving forward and back constantly, so the players only need to jump on them, wait a bit and drop down.

My approach was to add two links: the first from a close point near the platform into the platform itself, and a second one from the platform into a close point at the other side. The platform then has both NavmeshAdd and DynamicGridObstacle.

The AI properly detects that when I’m on the other side it should jump into the platform through the first link and use the second to come near me. Nevertheless, when the AI traverses the first link and jumps into the platform, the second link never has its coroutine called and it’s never set to traversing it.

I don’t know if I’m approaching this the wrong way. I’m using Kinematic Character Controller so I’m reading the steering target to set the movement for the player, but since the second link is never called I cannot stop the character and trigger my custom logic for moving through it.

Thanks for your help beforehand.

Nevertheless, when the AI traverses the first link and jumps into the platform, the second link never has its coroutine called and it’s never set to traversing it.

Hi there, I’m replicating this now to try and help- could you provide this section of code so I can see it for myself?

Hi again and sorry for the late response. I’ll try to provide as much info as possible.

I’m using Behavior Designer by Opsive, and in its most basic node I’m reading the steering direction of the FollowerEntity to manually set the movement through the joysticks as if a player was moving it.

if (_destinationSetter.target == null && m_Target.Value != null)
{
    _destinationSetter.target = m_Target.Value.transform;
}

var direction = (_agent.steeringTarget - transform.position).normalized;
if (_agent.reachedDestination || _agent.reachedEndOfPath || _agent.isTraversingOffMeshLink || direction == Vector3.zero)
{
    _controller.SetDirectionAndRotation(Vector3.zero);
    return TaskStatus.Success;
}

CheckMoveSpeed();
if (_blockToWalkSpeed)
{
    direction.x = Mathf.Clamp(direction.x, -_controller.m_playerKinematicModel.RunningInputThreshold, _controller.m_playerKinematicModel.RunningInputThreshold);
    direction.z = Mathf.Clamp(direction.z, -_controller.m_playerKinematicModel.RunningInputThreshold, _controller.m_playerKinematicModel.RunningInputThreshold);
}

_controller.SetDirectionAndRotation(direction);
return TaskStatus.Running;

As soon as the AI starts to move through the link, I send an event to the Behavior Tree with relevant information, such as which platform is jumping towards (for analyzing its state to prepare for the jump) or where the link ends.

[SerializeField]
private GameObject _platform;

private InputController _controller;

void OnEnable() => GetComponent<NodeLink2>().onTraverseOffMeshLink = this;
void OnDisable() => GetComponent<NodeLink2>().onTraverseOffMeshLink = null;

public IOffMeshLinkStateMachine GetOffMeshLinkStateMachine(AgentOffMeshLinkTraversalContext context)
{
    _controller = context.gameObject.GetComponent<AIController>();
    _controller.SetDirection(Vector3.zero);

    context.gameObject.GetComponent<BehaviorDesigner.Runtime.BehaviorTree>().SendEvent<object, object, object>("Link", context.link.relativeStart, context.link.relativeEnd, context.link.link.tag.value);
    context.gameObject.GetComponent<BehaviorDesigner.Runtime.BehaviorTree>().SetVariableValue("movingPlatform", _platform);
    return this;
}

IEnumerable IOffMeshLinkStateMachine.OnTraverseOffMeshLink(AgentOffMeshLinkTraversalContext ctx)
{   
    yield return null;
}

The Behavior Tree then will check through the tag which kind of movement the AI wants to do and, in this case since it’s jumping to a platform, it will wait for the platform to be close and idle (we’re now not reading the steering target, simply setting the direction of the inputs to 0), it will then jump to the platform and finish the sequence, starting again from the top for the node that reads the steering target.

public override TaskStatus OnUpdate()
{
    if (!_controller.isActiveAndEnabled)
    {
        return TaskStatus.Inactive;
    }

    if (m_stopWhenClose.Value)
    {
        var position = new Vector3(transform.position.x, 0, transform.position.z);
        // _stopPoint will be the position of the platform, ignoring its Y axis.
        float distance = Vector3.Distance(position, _stopPoint);
        if (distance <= m_stopDistance.Value)
        {
            _controller.SetDirection(Vector3.zero);
        }
    }

    if (_controller.m_KinematicCharacterMotor.GroundingStatus.IsStableOnGround)
    {
        if (m_stopWhenClose.Value)
        {
            _controller.SetDirection(Vector3.zero);
        }
        return TaskStatus.Success;
    }

    return TaskStatus.Running;
}

When the agent lands and we once again read it’s steering target it directs the controller back towards the beginning of the link, even though it is already on the platform and it should use the second link to get to its target, which would restart the same logic again (wait for the platform to stop, jump to the end of the link, and start back to move close to your target). Through setting breakpoints it seems that the second link does not start, so I’m unsure if I’m doing something wrong here.

Thanks for your help and once again sorry for taking a bit on providing more info!

Hi, I want to say that I finally got this working. I’m going to write down here how did I approach this problem in case it could help other users!

First of all, when the character starts its custom movement to jump to the platform (that being, its link has been used) I set the FollowerEntity.canMove property to false. The Behavior Tree will then be in charge of moving the character and making it jump towards the platform and, once it landed, I reset back the property to true. This seemed to solve many of the issues.

On the other hand, I found out that even though in one particular platform this solved all my problems, it wasn’t always true. I started to debug and found out that one cannot have two NodeLink2 components in the same GameObject with a custom IOffMeshLinkHandler. To solve this, I simply moved the NodeLink2 and its handler to two different GameObjects children of their original owner, and it worked perfectly.

Thanks for your help anyway and I hope this helps others that find the same problem as me!

1 Like

Oh that’s great, I’m glad you figured it out! And thank you very much for posting your solution and the code above, I’m sure it’ll come in handy to someone. Good luck with your project and let us know if you need any more assistance :saluting_face: