Animations, clips and motion. Log #5 (Unity, C#)
29.04.2017

          How will the motion work in Altos? Quite simply, when I click on position, I want my character to go to, I will set to the NavMeshAgent the target destination and start animation of my character. When the character reaches the target destination the agent stops and also the animation. In any other situation will character remain idle. As you may remember, my character has only one animation from Blender, which contains all clips. That's because animation in Unity can be splitted into individual clips.

          With my character object selected in the assets, click on the button Animations in the inspector. There is a filed called Clips in which should be two clips "DefaultTake" and "rig|rigAction". Both of them starts with frame 0, ends at frame 227. With "Default Take" selected, click on "+" symbol at the right bottom corner of Clips panel, this creates a new clip. Below Clips panel is a filed where you can rename the clip, below this filed is timeline and fields "Start" and "End" for frames. For each clip of my animation I will set start and end frames as required. Check Loop timeLoop pose if there are any clips that are required as loops. When everything is set, click on the button Apply at the bottom of Animation panel.

 

clips

 

         Next step will be setting the agent in motion. Let's create a new script PlayerController. In the assets create folders "Scripts>Entities>Player". Inside the folder Player right click and select Create>C#Script. Rename the file. Now, there are two options how to write or edit scripts. Either you can use free editor MonoDevelop or 30days trial version of VisualStudio both are provided by Unity. I am using VisualStudio. 

          It is very usefull to think it through, it really makes programming easier. As I said, I do not consider myself as a good coder so I apology for any kicks in the code. You can point them out in the comments section. And also I am not going to lecture about anything in coding, I suppose you can programme.

          Root of the click to move navigation is position of mouse click in the game world. But this is a little bit tricky. It's required to convert the mouse cursor coords into 3D world coordinates. Other words convert viewport coords into local position in 3D. Luckily the Unity has already function for this called ScreenPointToRay returning object Ray, which is straight line between mouse cursor position and point in the 3D. As the object type suggests it's required to create an object Ray in which I store the return value of function ScreenPointToRay. This function has one parameter - mouse position.

But I still don't know the positin of my mouse click. So I use function Physics.Raycast, which takes my ray and casts it through all objects in the scene, in dependency of its leghth. Very usefull parameter of the function Physics.Raycast is RaycastHit (rHit), providing information about first hit object. One of the information is variable point - it's the position where the ray comes through hit object. This is how i get the position of my mouse click - the position I want my character to go to. 

 

ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Input.GetMouseButton(0))
{
	if (Physics.Raycast(ray, out rHit, 100f))
	{
		// Move to position
	}
}

 

Now, when I have the position I can send the agent there. NavMeshAgent (nma) has variable destination, to which I assign my coordinates. Don't forget to call Resume() on the agent.

 

nma.destination = rHit.point;
nma.Resume();

 

          When the agent reaches the destination I will stop him. But that's actually not automatic nor easy. If I click in open area it stops without problems, but when I click, for instance, on tree or any object it never reaches the destination (considering the object as obstacle or static, otherwise agent will ignore this object, also if it has no collider raycast will ignore it too) and will never stop moving (playing animation).  It's necessary to reach the destination when I want to stop an animation. Agent does not have anything yet, that would properly returned its state, other words if the agent reached the destination or not. Well, it has some variables, but non of them works properly (pathPending, pathStatus). Therefore I have to calculate the distance. Agent has variable stoppingDistance, but in case where I click in open area of the terrain I need this distance to be almost zero. So my solution is, that every object in scene has its own radius, determining minimal distance on which will agent stop. When I click on object I get the minimal stopping distance and set it to agent. To calculate the distance to the target position I need this radius. Another factor affecting stopping distance is agents Base Offset. The origin of the agent is in the middle of the height of the cylinder (NavMeshAgents cylinder). That means its position is above the ground, thus it would never reached the target position. It's really odd, can't tell why, but even this offset size is not the number I am looking for. Using the Debug.Log and console I found out it's actually 1.85 fold the offset. Eventually I just distinguish between clicking on object or terrain and then I set the stopping distance. By setting a tag to objects I can simply say on what I clicked. Every object in the inspector has select box where you can add your own tags. But there is relatively easier way how to exclude non-terrain objects, which I will describe later. Here is whole code.

 

void Update()
    {
        // Movement
        ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (Input.GetMouseButton(0))
        {
            if (Physics.Raycast(ray, out rHit, 100f))
            {
                if (rHit.transform.tag != "Player")
                    MoveToPosition();
            }
        }
        
        destinationDistance = Vector3.Distance(transform.position, rHit.point) - (nma.baseOffset * 1.85f) - nma.stoppingDistance;
        if (rHit.transform != null && rHit.transform.tag != "Player")
        {
            if (rHit.transform.tag == "Ground")
            {
                nma.stoppingDistance = 0.1f;
                if (destinationDistance <= 0.1f)
                {
                    anim.SetBool("Run", false);
                    nma.Stop();
                }
            }
            else
            {
                stoppingDistance = GetEntityStopRadius(rHit.transform);
                nma.stoppingDistance = stoppingDistance;

                if (destinationDistance <= stoppingDistance)
                {
                    anim.SetBool("Run", false);
                    nma.Stop();
                }
            }
        }
    }

    void MoveToPosition()
    {
        nma.destination = rHit.point;
        anim.SetBool("Run", true);
        nma.Resume();
    }

    float GetEntityStopRadius(Transform entity)
    {
        return entity.GetComponent().StoppingDistance;
    }

 

I will return to lines anim.SetBool("Run", true); and anim.SetBool("Run", false); in the next log.