If you've been wondering how to implement the conventional PC-style mouse-driven interactions in ORK, then pay attention! :-)

This is the type of interaction where you click on an object (a chest, an NPC, etc) and your character pathfinds to the object and then interacts with it when he arrives. If you click somewhere else on the ground while the character is moving to the object, he will stop moving towards the object and the interaction event will not continue.

I am assuming the use of the Unity Navigation API and a baked NavMesh for this tutorial, but you should be able to adapt this approach to use another pathfinding solution.

This tutorial uses a small set of custom event steps that I wrote to replace a custom component and a few Field-To-Variable steps from the previous version of this tutorial. Download it now (Dropbox link).

EDIT: if you're using ORK 2.6.3, this dll might break things. Use the alternative steps I list on the following page. Will update this tutorial soon.

Import the OrkNavMeshSteps.dll file into your project. I created a sub-folder under "ORK Framework" called "Plugins" and put it in there.

Part One: The Move Event

First up, we'll create a Move Event that moves the character (using the NavMesh) to the object that has been clicked on. This event will be called by the Game Events that you set up on objects in the scene. I have designed this event to be reusable throughout the game, regardless of what type of object the player clicks on. The event sets the NavMesh destination and then waits either for the character to reach the destination or for the navigation to be interrupted by setting a new destination. The event completes by setting two global flags that are checked by the events that called the Move Event.

Create a new Move Event. I've named mine moveToInteractionPoint.

The final result should look something like this:

image

1. Add a Change Game Variables step.

Create a global bool variable named MoveToInteractionComplete and set it to false:

image


2. Add a Change NavMesh Target step.

Ensure the Moving Object is set to Actor.

The Position should be set to a global Game Variable with the key CurrentInteractionPoint. We haven't created this yet; it will be set any event that calls this one.

image

3. Add a Wait step.

Set it to 0.1s. This is simply to give the NavMeshAgent enough time to set the destination.


4. Add the custom Check Remaining Distance step.

You can find it under Movement/NavMesh or Check/Movement/NavMesh.

image

If the remainingDistance along the calculated NavMeshPath is less than the specified value, then we're finished moving. If it is not, then we enter a loop that keeps checking this value until the character has reached the destination.


4. Add a Wait step.

Connect the "Success" terminal of step 4 here.

Set the wait time to 0.3s. You might need to tweak this setting. This step is here to allow the walk/run animations time to finish before passing control back to the calling event, which might play an animation on the character who was moving.

5. Add a Change Game Variables event.

If we have reached this node, then the character has reached the object that is being interacted with. Thus, we will signal to the calling event both that this move event has completed and that it should continue with the interaction event:

image

We are setting two global bool variables to true: MoveToInteractionComplete and ContinueInteractionEvent.

Now we need to handle cases where the player clicks somewhere else on the NavMesh and interrupts the navigation initiated by this event.

6. Add the custom Check Destination Changed step.

You can find it under Movement/NavMesh or Check/Movement/NavMesh.

Connect the "Failed" terminal of step 4 here.

This step calculates the distance between the original NavMesh destination (stored in CurrentInteractionPoint) and the current destination of the actor's NavMeshAgent. The check value specified here is the distance threshold at which we will decide that the destination has been changed. When I was getting this value via a custom component, I found that the distance was consistently about 0.0667 units, so I settled on a default check value of 0.1.

image

Connect the "Failed" terminal back to the Wait node that we added in step 3. This completes the loop in which this event will spend most of its execution time. The loop will end either when the destination has been reached or the player sets a new destination.

7. Add a Change Game Variables event.

If we have reached this node, then the character's navigation to the object has been interrupted. Thus, we will signal to the calling event both that this move event has completed and that it should exit the interaction event:

image

We are setting two global bool variables: MoveToInteractionComplete to true, and ContinueInteractionEvent to false.


And that's it for the Move Event.
Post edited by Keldryn on
  • edited January 2016
    Part Two: An Example Interaction Event

    In this example, when the player clicks on a chest, the character will walk to the chest, open it, access an ItemCollector (UI) to retrieve its contents, and then close the chest at the end.

    The first thing we need to do is decide how we want to set up our object in the scene. For something like a chest, we don't want the player to be able to walk through it. So we have two options. We can make it a static object and thus have it included in the NavMesh bake, or we can attach a NavMeshObstacle component to it so that it cuts into the NavMesh at run-time. For a smaller object like a chest, I would probably use a NavMeshObstacle in case I ever decide that objects of that size could potentially be moved or destroyed.

    In either case, the Agent Radius setting (defaults to 0.5) when you bake the NavMesh will create a non-walkable buffer around such objects, so you can't simply use the position of the object itself as your destination.

    My solution is to add an empty GameObject called Interaction Point to the Prefab. As a general rule, I always like to put any game models as children of another GameObject, so it's easy to swap models without breaking all of the prefab references. I add this Interaction Point to any prefab that I want to attach an Interaction Event to.

    image

    In this image, you can see that I have offset the Interaction Point object at 0.5 units on the Z-axis (which is normally the front of an object). This not only ensures that we don't get a destination point that is in the non-walkable area, it also ensures that any animations played on the character will always happen in the same location and at the same distance from the event object.

    This chest model is from the 3DForge Fantasy Treasure Loot Kit and it has opening and closing animations provided for it. I will hook up the animations in this event, but they are entirely optional.

    If you don't have an appropriate model, you can always just use a cube primitive and skip the animation steps.

    So you should have a WoodenChest_prefab with the Interaction Event component attached to it. The actual 3D model will be parented to the prefab, as will the Interaction Point object.

    Also, create an empy GameObject parented to the chest prefab and attach an ItemCollector component to it. Set the Collection Type to Box.

    Now that we've gotten that out of the way, let's create an event.

    This is a GameEvent called useContainer, and the final result looks like this:

    image

    0. Event Settings node

    This event needs three actors added in the Event Settings step:
    Actor 0 - Type: Player
    Actor 1 - Type: Object; check Event Object (this is the prefab that has the event component)
    Actor 2 - Type: Object (this is the ItemCollector)

    We will need to add the ItemCollector to the Interaction Event in the Inspector.

    image

    Also note that we have Max Click Distance set to -1.


    1. Add a Transform to Variable step.

    image

    Here's where we set our CurrentInteractionPoint global variable used in the MoveToInteractionPoint event. We're getting the transform of the Interaction Point child of the event object (the chest prefab).

    2. Add a Call Move Event step.

    image

    Here's where we call our MoveToInteractionPoint event. We want this to use the Player (Actor 0) as the moving object.

    3. Add a Wait step.

    Set to 0.1s. We're just ensuring that we've allowed enough time for the Move Event to get started up.

    4. Add a Check Game Variables step.

    Connect the "Failed" terminal back to the Wait node in step 3.

    image

    This is the control condition for this event's loop. We're watching for the MoveToInteractionComplete global variable to be set by the Move Event. This doesn't tell us anything about whether or not the navigation was cancelled or if the destination was reached; all that we care about at this stage is that MoveToInteractionPoint is finished.


    5. Add a Check Game Variables step.

    Connect the "Success" terminal of the Check Game Variables node in step 4 here.

    image

    Now that we know that the MoveToInteractionPoint event has finished, we will check to see if we should continue with this interaction event or if we should simply exit gracefully. Of course we know that this is determined by whether or not the navigation was cancelled in MoveToInteractionPoint, but this event only needs to know whether or not it should proceed or exit.

    6. Add a Block Move AI step.

    image

    While we do want NPCs and party members to continue moving while the Player is navigating to the event object, we want to stop them from moving once the Player starts the interaction. This helps stop party members from getting stuck in walking animations while the Player and chest animate.

    7. (Optional) Add a Combatant Animation step.

    Here we start playing some sort of "use item" animation that we have set up on the Player combatant.
    We won't set a wait in this step, as we want to start animating the lid of the chest opening before the Player stops animating. Depending on the timing of your animations, you may need to add a Wait step between this and the next step.

    image


    8. (Optional) Add a Call Function step.

    image

    Instead of calling a custom component, you could add a step to play a Legacy animation on the chest, but this requires you to specify the name of the child object (the mesh) and the names of the animations.

    To make this event usable on any type of container (that can be opened or closed), we will add this component to our event object prefab:

    using UnityEngine;

    public class Container : MonoBehaviour
    {
    public AnimationClip openAnimation;
    public AnimationClip closeAnimation;


    private void Start()
    {
    _animation = GetComponentInChildren<Animation>();
    }

    public void Open()
    {
    if (_isOpen)
    return;

    _animation.Stop();
    _animation.Play(openAnimation.name);
    _isOpen = true;
    }

    public void Close()
    {
    if (!_isOpen)
    return;

    _animation.Stop();
    _animation.Play(closeAnimation.name);
    _isOpen = false;
    }

    private Animation _animation;
    private bool _isOpen;
    }



    When attached to the object prefab, the inspector allows us to set the appropriate open and close animations without having to hard-code the animation names in the event:

    image

    Note that this component goes on the parent prefab object and not the mesh that contains the Animation component. Using GetComponentInChildren() allows us to not have to hard-code the name of the child object in the event.

    9. Add a Wait event.

    I set it to 1.33s because that's the length of the "Use" animation clip. The timing of the character animation and the chest animation are close enough for me, so I'm not getting too fiddly with the timing.

    10. Create a Start Item Collector event.

    image

    I have the ItemCollector component on an empty GameObject parented to the main object prefab.

    11. (Optional) Add a Call Function step.

    image

    This is the same as step 8, except now we're calling the Close() method on the Container component.

    12. Add a Block Move AI step.

    Now we un-block the Move AI that we blocked back in step 6.

    13. Add a Change Game Variables step.

    We'll finish off this event by resetting all of the global variables that we've used:

    image

    We are removing the CurrentInteractionPoint (Vector3) because we don't want to set it to {0,0,0} as that is potentially a valid coordinate in the scene.

    We'll set ContinueInteractionEvent to false to reset it before the next Interaction Event is run. We probably don't actually need to do this, as it should be set properly before it is ever checked, but we'll be on the safe side.

    Connect the "Failed" terminal of the Check Game Variables node in step 4 here as well. If the player clicks on the chest and then clicks somewhere else on the NavMesh while the character is still moving towards the chest, then the UseContainer even will simply clean up its variables and exit without doing anything with the event object.

    In my node diagram up the at top, you may notice a Show Dialogue step that displays a "Navigation Cancelled" message. I had it set to the "Top Info" bar with auto-close turned on. I wouldn't keep this in an actual game, but it was useful for testing purposes.

    And that's it! We can use this event as a guide for building any other interactions where we want the character to walk to the clicked object before starting the interaction.
    Post edited by Keldryn on
  • Awesome tutorial, Keldryn! :)

    Then again, when we bought Ork, many of us thought that such a basic RPG operation would be doable out of the box. I mean games from the early 90's are capable of this sort of interaction, being a staple. As @chud575 suggested, this could be integrated right into the framework. Just a thought.
  • I would love it if the framework did have support for this, but I don't think it's fair to criticize it for not doing do.

    ORK handles direct control paradigms just fine out of the box. If you're doing a first person RPG (Elder Scrolls, Bethesda Fallout), an over-the-shoulder 3rd person RPG (Gothic, Mass Effect), or anything designed to use a gamepad (JRPGs in general), then you're set.

    It's only this indirect control scheme -- where you click somewhere and your character pathfinds his way there -- that ORK needs a lot of work to implement it properly. It's been a common control scheme since Diablo and Fallout (and Ultima VII in an early form), but it works very differently from the direct control schemes. There is much more cross-over in functionality between the variations of direct control.

    That being said, of course I would love to see it as an option you just have to check in the Editor and it all works. But I'm not expecting that to happen.
  • edited January 2016
    Maybe Gil will put this on his to do list, who knows. He may not even have realized it could be done as we can't expect him to know every possible gameplay tactic.

    Meanwhile, thank you for this. It must have been very satisfying for you to work this out and it's certainly satisfying to know it can be done.

    One of the beauties of ORK is it is very flexible. It's a real treasure chest of possibilities.

    I wish this would work on mobile. : D Maybe it would. I might try it.

    I just have one question--your script is C# not Javascript? Am still a novice coder so have to ask.

    Post edited by Catacomber on
  • I don't see why it wouldn't work on mobile, as long as the NavMesh destination can be set via touch.

    Yes, the script is C#. I do all of my coding for Unity in C#. I save JavaScript for web development (I.e. when I have to).

    And it was highly satisfying to finally work this out. I'd been thinking about it for a while and was sort of avoiding it, but it turned out to be a lot easier than I had thought it would be (the same goes for writing custom event steps).

    I'm trying it out with talking to NPCs now and it works well with a few tweaks (such as blocking the event object's Move AI). Will post that example when I'm happy with it.
  • edited January 2016
    @Gregorik - yeah gotta agree with @Keldryn here, you know that's what dev cycles are for. What this framework can do is outstanding, and this feature im sure will find its way into the framework natively at some point. Sounds like before me not too many people were messing around with this style, which is why GiL is running into the problems now (I have more to post, but hey one thing at a time).

    @Keldryn - in your first post - you have an Actor called "Moving Object". How did you achieve that? Everytime i add an actor to an event (or most everything else) it gets a '#:' in front of it, like in your second example. Makes me think im doing something wrong while copying your example. Must've not used a "Move Event"
    Post edited by chud575 on
  • Great tute, thanks :)
  • anyone know how to load a plugin? I keep getting this error:
    Plugin (OrkNavMeshSteps): Assembly (OrkNavMeshSteps) or namespace (OrkNavMeshSteps) not found:
    at System.Collections.Generic.Dictionary`2[System.Type,System.Reflection.ConstructorInfo].ContainsKey (System.Type key) [0x0000b] in /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Collections.Generic/Dictionary.cs:458
    at ORKFramework.ReflectionTypeHandler.CreateInstance (System.Type type) [0x00000] in
  • edited January 2016
    If the plugin is just for the event steps, you don't even need to add it to ORK's settings - event/formula/battleAI steps are automatically available when they're in the Unity project.

    @Keldryn
    Great work - thanks for putting this together :)
    Post edited by gamingislove on
    If you're enjoying my products, free updates and support, please consider supporting me on patreon.com!
  • sigh - not seeing this custom "Check Destination Changed" node. Both of mine are still the original
  • Gave it a quick try - imported the DLL into the project and the steps are there.
    The steps are in Movement > NavMesh, otherwise you can also use the Search to find steps :)
    If you're enjoying my products, free updates and support, please consider supporting me on patreon.com!
  • ah i see - okay, change of what's missing then, I have the custom additions, like "Check Destination Changed", but after setting "Check Value" value type to Global, i no longer have a "Check Value" box. @Keldryn i guess this one's for you.
  • Huh, that's weird. :-P

    @gamingislove, any idea why setting "Global" would make the Check Value box disappear? This was my first time writing my own event steps and I based the structure on the built-in steps.

    @chud575, I'll re-post the alternative steps for checking if the destination was changed (two event steps and a short custom component) that you can use until we get this figured out. Probably in about 3 hours; I just woke up over here in western Canada.
Sign In or Register to comment.