Unity 3.x Scripting-Character Controller versus Rigidbody

Packt Publishing

Write efficient, reusable scripts to build custom characters, game environments, and control enemy AI in your Unity game with this book and ebook.

Creating a controllable character

There are two ways to create a controllable character in Unity, by using the Character Controller component or physical Rigidbody. Both of them have their pros and cons, and the choice to use one or the other is usually based on the needs of the project. For instance, if we want to create a basic role playing game, where a character is expected to be able to walk, fight, run, and interact with treasure chests, we would recommend using the Character Controller component. The character is not going to be affected by physical forces, and the Character Controller component gives us the ability to go up slopes and stairs without the need to add extra code. Sounds amazing, doesn't it? There is one caveat. The Character Controller component becomes useless if we decide to make our character non-humanoid. If our character is a dragon, spaceship, ball, or a piece of gum, the Character Controller component won't know what to do with it.

It's not programmed for those entities and their behavior. So, if we want our character to swing across the pit with his whip and dodge traps by rolling over his shoulder, the Character Controller component will cause us many problems.

In this article, we will look into the creation of a character that is greatly affected by physical forces, therefore, we will look into the creation of a custom Character Controller with Rigidbody, as shown in the preceding screenshot.

Custom Character Controller

In this section, we will write a script that will take control of basic character manipulations. It will register a player's input and translate it into movement. We will talk about vectors and vector arithmetic, try out raycasting, make a character obey our controls and see different ways to register input, describe the purpose of the FixedUpdate function, and learn to control Rigidbody.

We shall start with teaching our character to walk in all directions, but before we start coding, there is a bit of theory that we need to know behind character movement.

Most game engines, if not all, use vectors to control the movement of objects. Vectors simply represent direction and magnitude, and they are usually used to define an object's position (specifically its pivot point) in a 3D space. Vector is a structure that consists of three variables—X, Y, and Z. In Unity, this structure is called Vector3:

To make the object move, knowing its vector is not enough.

Length of vectors is known as magnitude. In physics, speed is a pure scalar, or something with a magnitude but no direction. To give an object a direction, we use vectors. Greater magnitude means greater speed. By controlling vectors and magnitude, we can easily change our direction or increase speed at any time we want.

Vectors are very important to understand if we want to create any movement in a game. Through the examples in this article, we will explain some basic vector manipulations and describe their influence on the character. It is recommended that you learn extra material about vectors to be able to perfect a Character Controller based on game needs.

Setting up the project

To start this section, we need an example scene. Perform the following steps:

Select Chapter 2 folder from book assets, and click on on the Unity_chapter2 scene inside the custom_scene folder.

In the Custom scripts folder, create a new JavaScript file. Call it CH_Controller (we will reference this script in the future, so try to remember its name, if you choose a different one):

In a Hierarchy view, click on the object called robot. Translate the mouse to a Scene view and press F; the camera will focus on a funny looking character that we will teach to walk, run, jump, and behave as a character from a video game.

Creating movement

The following is the theory of what needs to be done to make a character move:

Register a player's input.

Store information into a vector variable.

Use it to move a character.

Sounds like a simple task, doesn't it? However, when it comes to moving a player-controlled character, there are a lot of things that we need to keep in mind, such as vector manipulation, registering input from the user, raycasting, Character Controller component manipulation, and so on. All these things are simple on their own, but when it comes to putting them all together, they might bring a few problems. To make sure that none of these problems will catch us by surprise, we will go through each of them step by step.

Manipulating character vector

By receiving input from the player, we will be able to manipulate character movement. The following is the list of actions that we need to perform in Unity:

Open the CH_Character script.

Declare public variables Speed and MoveDirection of types float and Vector3 respectively. Speed is self-explanatory, it will determine at which speed our character will be moving. MoveDirection is a vector that will contain information about the direction in which our character will be moving.

Declare a new function called Movement. It will be checking horizontal and vertical inputs from the player.

Finally, we will use this information and apply movement to the character. An example of the code is as follows:

Register input from the user

In order to move the character, we need to register an input from the user. To do that, we will use the Input.GetAxis function. It registers input and returns values from -1 to 1 from the keyboard and joystick. Input.GetAxis can only register input that had been defined by passing a string parameter to it. To find out which options are available, we will go to Edit | Projectsettings | Input. In the Inspector view, we will see Input Manager.

Click on the Axes drop-down menu and you will be able to see all available input information that can be passed to the Input.GetAxis function. Alternatively, we can use Input.GetAxisRaw. The only difference is that we aren't using Unity's built-in smoothing and processing data as it is, which allows us to have greater control over character movement.

To create your own input axes, simply increase the size of the array by 1 and specify your preferences (later we will look into a better way of doing and registering input for different buttons).

this.transform is an access to transformation of this particular object. transform contains all the information about translation, rotation, scale, and children of this object. Translate is a function inside Unity that translates GameObject to a specific direction based on a given vector.

If we simply leave it as it is, our character will move with the speed of light. That happens because translation is being applied on character every frame. Relying on frame rate when dealing with translation is very risky, and as each computer has different processing power, execution of our function will vary based on performance. To solve this problem, we will tell it to apply movement based on a common factor—time:

this.transform.Translate(MoveDirection * Time.deltaTime);

This will make our character move one Unity unit every second, which is still a bit too slow. Therefore, we will multiply our movement speed by the Speed variable:

this.transform.Translate((MoveDirection * Speed) * Time.deltaTime);

Now, when the Movement function is written, we need to call it from Update. A word of warning though—controlling GameObject or Rigidbody from the usual Update function is not recommended since, as mentioned previously, that frame rate is unreliable. Thankfully, there is a FixedUpdate function that will help us by applying movement at every fixed frame. Simply change the Update function to FixedUpdate and call the Movement function from there:

function FixedUpdate (){
Movement();
}

The Rigidbody component

Now, when our character is moving, take a closer look at the Rigidbody component that we have attached to it. Under the Constraints drop-down menu, we will notice that Freeze Rotation for X and Z axes is checked, as shown in the following screenshot:

If we uncheck those boxes and try to move our character, we will notice that it starts to fall in the direction of the movement. Why is this happening? Well, remember, we talked about Rigidbody being affected by physics laws in the engine? That applies to friction as well. To avoid force of friction affecting our character, we forced it to avoid rotation along all axes but Y. We will use the Y axis to rotate our character from left to right in the future.

Another problem that we will see when moving our character around is a significant increase in speed when walking in a diagonal direction. This is not an unusual bug, but an expected behavior of the MoveDirection vector. That happens because for directional movement we use vertical and horizontal vectors. As a result, we have a vector that inherits magnitude from both, in other words, its magnitude is equal to the sum of vertical and horizontal vectors.

To prevent that from happening, we need to set the magnitude of the new vector to 1. This operation is called vector normalization. With normalization and speed multiplier, we can always make sure to control our magnitude:

Jumping

Jumping is not as hard as it seems. Thanks to Rigidbody, our character is already affected by gravity, so the only thing we need to do is to send it up in the air. Jump force is different from the speed that we applied to movement. To make a decent jump, we need to set it to 500.0). For this specific example, we don't want our character to be controllable in the air (as in real life, that is physically impossible). Instead, we will make sure that he preserves transition velocity when jumping, to be able to jump in different directions. But, for now, let's limit our movement in air by declaring a separate vector for jumping.

User input verification

In order to make a jump, we need to be sure that we are on the ground and not floating in the air. To check that, we will declare three variables—IsGrounded, Jumping, and inAir—of a type boolean. IsGrounded will check if we are grounded. Jumping will determine if we pressed the jump button to perform a jump. inAir will help us to deal with a jump if we jumped off the platform without pressing the jump button. In this case, we don't want our character to fly with the same speed as he walks; we need to add an airControl variable that will smooth our fall.

Just as we did with movement, we need to register if the player pressed a jump button. To achieve this, we will perform a check right after registering Vertical and Horizontal inputs:

GetButtonDown determines if we pressed a specific button (in this case, Space bar), as specified in Input Manager. We also need to check if our character is grounded to make a jump.

We will apply vertical force to a rigidbody by using the AddForce function that takes the vector as a parameter and pushes a rigidbody in the specified direction. We will also toggle Jumping boolean to true, as we pressed the jump button and preserve velocity with JumpDirection:

To make sure that our character doesn't float in space, we need to restrict its movement and apply translation with MoveDirection only, when our character is on the ground, or else we will use jumpDirection.

Raycasting

The jumping functionality is almost written; we now need to determine whether our character is grounded. The easiest way to check that is to apply raycasting. Raycasting simply casts a ray in a specified direction and length, and returns if it hits any collider on its way (a collider of the object that the ray had been cast from is ignored):

To perform a raycast, we will need to specify a starting position, direction (vector), and length of the ray. In return, we will receive true, if the ray hits something, or false, if it doesn't:

As we have already mentioned, we used transform.position to specify the starting position of the ray as a center of our collider. -transform.up is a vector that is pointing downwards and collider.height is the height of the attached collider. We are using half of the height, as the starting position is located in the middle of the collider and extended ray for two units, to make sure that our ray will hit the ground. The rest of the code is simply toggling state booleans.

Improving efficiency in raycasting

But what if the ray didn't hit anything? That can happen in two cases—if we walk off the cliff or are performing a jump. In any case, we have to check for it.

If the ray didn't hit a collider, then obviously we are in the air and need to specify that. As this is our first check, we need to preserve our current velocity to ensure that our character doesn't drop down instantly.

Raycasting is a very handy thing and being used in many games. However, you should not rely on it too often. It is very expensive and can dramatically drop down your frame rate.

Right now, we are casting rays every frame, which is extremely inefficient. To improve our performance, we only need to cast rays when performing a jump, but never when grounded. To ensure this, we will put all our raycasting section in FixedUpdate to fire when the character is not grounded.

To determine if our character is not on the ground, we will use a default function— OnCollisionExit(). Unlike OnControllerColliderHit(), which had been used with Character Controller, this function is only for colliders and rigidbodies. So, whenever our character is not touching any collider or rigidbody, we will expect to be in the air, therefore, not grounded.

Let's hit Play and see our character jumping on our command.

Additional jump functionality

Now that we have our character jumping, there are a few issues that should be resolved. First of all, if we decide to jump on the sharp edge of the platform, we will see that our collider penetrates other colliders. Thus, our collider ends up being stuck in the wall without a chance of getting out:

A quick patch to this problem will be pushing the character away from the contact point while jumping. We will use the OnCollisionStay() function that's called at every frame when we are colliding with an object. This function receives collision contact information that can help us determine who we are colliding with, its velocity, name, if it has Rigidbody, and so on. In our case we are interested in contact points. Perform the following steps:

Declare a new private variable contact of a ContactPoint type that describes the collision point of colliding objects.

Declare the OnCollisonStay function.

Inside this function, we will take the first point of contact with the collider and assign it to our private variable.

Add force to the contact position to reverse the character's velocity, but only if the character is not on the ground.

Declare a new variable and call it jumpClimax of boolean type.

Contacts is an array of all contact points.

Finally, we need to move away from that contact point by reversing our velocity. The AddForceAtPosition function will help us here. It is similar to the one that we used for jumping, however, this one applies force at a specified position (contact point):

The next patch will aid us in the future, when we will be adding animation to our character later in this article. To make sure that our jumping animation runs smoothly, we need to know when our character reaches jumping climax, in other words, when it stops going up and start a falling.

In the FixedUpdate function, right after the last else if statement, put the following code snippet:

else if (inAir&&rigidbody.velocity.y == 0.0) {
jumpClimax = true;
}

Nothing complex here. In theory, the moment we stop going up is a climax of our jump, that's why we check if we are in the air (obviously we can't reach jump climax when on the ground), and if vertical velocity of rigidbody is 0.0. The last part is to set our jumping climax to false. We'll do that at the moment when we touch the ground:

Running

We taught our character to walk, jump, and stand aimlessly on the same spot. The next logical step will be to teach him running. From a technical point of view, there is nothing too hard. Running is simply the same thing as walking, but with a greater speed. Perform the following steps:

Declare a new variable IsRunning of a type boolean, which will be used to determine whether our character has been told to run or not.

Inside the Movement function, at the very top, we will check if the player is pressing left or right, and shift and assign an appropriate value to isRunning:

Cameras

They are important! It does not matter what discipline an individual is in. A camera and its uses are crucial to the development of the game and/or positions that the players will find themselves in. We will build a generic camera script that we will be able to configure for various positions.

Camera scripting

The camera script is quite heavy when it comes to scripting. So, we are going to first script functionality for the fps camera, which will form the basic structure for the other types of cameras.

It will come down to the following steps:

Create the camera script.

Write the camera switching functions.

Write the camera movement functionality.

Influence character movement through camera positioning.

Creating camera script

First, we will create a JavaScript named CameraScr. In that, we will have the functionalities for the reader to be able to manipulate various properties for the camera setup, such as the height that the camera will sit at and the distance from which the camera will be located from the target. In the script:

We will set up some simple variables

There will be a variable for the object to be tracked, another for distance, for height offset, side offset, smooth follow, and the current camera type

In the case of the object to be tracked, this will be the GameObject character. Make sure to set its type as Transform and make the variable public. We will use a list of variables of specified types to store multiple values of a specific type in a single variable. Be sure to use the square brackets after the following variable types and make them public, as we will be adding values to them in the Inspector menu:

The second variable is the current camera state and will be of the type int and defaulted to 0. It is okay if it is private.

The third variable is for the distance, which we define as the camera distance and have its type defined as float.

The fourth variable is for height and its type is again float. This variable will handle the height offset for the camera.

At this point, we only care about the values that are in position 0 of the array variables.

Creating an enumeration list

Next, we will set up an enumeration that will deal with switching the values for the different camera types. An enumeration is a variable that can hold integer values in any form. The user just needs to keep in mind that whatever is put into an enum, enumeration for short, will be converted into an integer. The first value in an enum is considered in the first spot, the second in the second spot, and so on. Create an enum and call it CamType.

Remember that enum is like a class or list of variables (names in this case) and must use curly braces to begin and end its statement. Inside enum, create the camera types (FP, SP, TP). The camera types FP, SP, and TP, will have the variable integer values of 0, 1, and 2.

In order not to get an error at this point, you will have to create a variable, of type enum. Call it CamType. This variable allows the reader to change the enum type at will in the Inspector menu. The variable may be private but remember, if you wish to change the type in Inspector, it must be public. The variable and enum should resemble the following code snippet:

private var cameraType : CamType;
enum CamType { FP, SP, TP }

Writing functions

For now, we have taken care of variables, and we have to begin to write the functions.

The Initialize function

We will start with giving values to our variables. Perform the following steps:

Right off the bat, we will need to write an Initialize function. In this function, we want to change the camera type to the default camera type, which we want the character to start off with. In this case, it is camera 1.

This is based upon the camera's value in enum.

After that, we will need to change the camera enum type to the first-person camera.

Set charObj with received Player value.

Right now, the Initialize function will look similar to the following code snippet:

Changing camera function

The changing camera function will be called ChangeCamType. As its name implies, this function will change the camera from one type to another.

In this function, we need to check a couple of things, such as identify the current camera type and then change the camera to the next type. Perform the following steps:

First, create the ChangeCamType function.

Check for the camera number, inside of which we want to use the same camera switching line that we used in the Initialize function. After that line, we want to state that the camera number is now equal to the next camera type. This function should look similar to the following code snippet:

Now that we have the camera switching, we need to assign the list variable values to our equation variables. To do this, we will use a switch case statement to change the equation variables based upon the current camera type.

A switch statement is pretty much an if statement except that you can switch variable values without having to reassign them. This type of statement works great for Artificial Intelligence (AI) behaviors and will be used later on in the book for just that purpose.

Changing the camera values function

The changing camera values function will be called SetCamValues. Perform the following steps:

The first thing is to call the ChangeCamType function.

For the switch statement, the first thing that we have to check is that camera type is true. After that, we can use a case statement to switch variables based upon the current camera type. The first case statement will check for when the current camera is first person.

Now, we will create the equation variables. These variables will be used to hold the values from the selected array variables and in the final equations that will determine the final setup of the camera. This is done so that there can be one block of code for all of the camera types instead of a block of code for each of the camera types. Each of these variables will have the same type, minus the square brackets, of the values which they will be taking on but can be made private if preferred.

The variables to be created are as follows:

camDist: This variable will deal with camera distance list variable

hOffset: This variable will deal with height offset list variable

Inside the case statement, after the reader has matched all of the equation variables with the list variables, we need to make sure that the right list number has been assigned to the variable. For camera number 1, FP, the number is one but with lists, as in most scripting or programming languages, they start at 0. So, make sure that the list variables that the equation variables are equaling, have 0 in the brackets for FP, 1 for SP, and 2 for TP.

At the end of the case statement, we then want to put a break line in. This break line prevents the code from moving on to the next case statement.

In the Initialize function, at the end of it, we want to add this function in there as well. The following is an example of the code:

Character movement and camera positioning

Now for secondary functionality of the camera, orbiting and character movement based upon camera positioning and the coding for the two other cameras. Third-person view camera is demonstrated in the following screenshot:

Updating camera type changing

First we will tackle the two other cameras as they are the easier of the two functionalities to implement. Let's venture back to the ChangeCamType function. In here, we only had a change statement for one camera. Now we need to add the other two in. Perform the following steps:

We just need to copy and paste the existing if statement two times.

Change the if statements to the else if statements. The camera numbers should be from 1 to 2 for the middle statement and 1 to 3 for the lower statement.

The CampType value for the middle statement should be changed from SP to TP as well and for the lower statement, SP to FP.

Lastly, the camera numbers found within the if statement blocks should be changed to 3 for the middle one and 1 for the lower one.

These statements allow the camera to change its enum type whenever the T button is pressed. This function should now look like the following code snippet:

Clamping angles

The last function to write for this script is the ClampAngle function. It has the following characteristics:

The ClampAngle function is going to be taking three parameters.

Those parameters are angle, min, and max.

There will be two if statements in the function and then a return function.

The parameters that are coming in are the angle, which we want to check and see if it is smaller or greater than 360 degrees. If greater, we subtract 360 so that the angle becomes within the acceptable range. If lower, we add 360 degrees. We return the result back to the function so that it makes sure that the angle never goes out of range.

Now that everything is compiled, we need to go back to Inspector and add in the values for the list variables and the target object. These variables can be set to your own discretion but the following is a screenshot of our values:

Camera's late update

There is one more function to write for this stage of the camera and that is the LateUpdate function.

This function is used because during the Update function, the target object of the script might have moved beyond an area where the camera can see, that is, inside of a building. This function will handle the calling of the remaining functions. Perform the following steps:

Create the function and inside of it, do a simple check to make sure that a target exists (charObj).

Inside of this check, we want to call the Apply function.

The following code snippet shows what it should look like:

function LateUpdate(){
If(charObj)
Apply();
}

Rotating character with a camera

One more function before we are done. In the Character Controller script, inside of the FixedUpdate function, right before the calling of the Movement function, we will add the following line of code:

This line allows the character to rotate with the rotation of the camera. We grab the mouse x and rotate the character by it and we dampen it by the delta time to make sure that it becomes smooth gradually. The following code snippet, which shows the complete CamerScr script is the final code:

Congratulations! You can now have a camera rig that will give you a lot of functionality in a small limited package.

Animation controls

In the last part of this article, we will talk about what makes games look awesome—animations. We will learn how to control animations through code, learn the truth about the Start and Awake functions, and figure out how to make smooth transactions in between animations.

Playing simple animations

Time to add some visual indication to our movement and jump into the world of animations. Thankfully, we don't have to worry about animating our character, all animations are already done for us and are included with the model.

In this section, we will talk about basic animations and how to play them. As our game continues to grow, we will add more advanced techniques to handle various animations. Let's create a new script whose main purpose will be to handle and control all animations for our character, such as their speed, play order, and modes. Perform the following steps:

Create a new script in the Custom scripts folder and call it CH_Animation.

Declare a private variable of a CH_Controller type (script that handles movement, if you name it differently, use your name to declare its type), call it Controller. This way we can reference any scripts, just by declaring them with a type of script's name.

Start function versus Awake function

Let's talk a bit about the difference between these two functions. At first glance, there is none, and many people make the same mistake by mismatching them. This is a mistake that can lead to problems.

The Awake() function is the first function that is called when you start a game. Right after you press the Play key, the engine goes through all scripts and executes the Awake function in each of them.

The Start() function is called right after all the Awake() functions on all objects are executed.

We can give both these functions a small test. Let's test this:

Add debug logs in both of these functions. In Awake, write something like I'm awake and I'm ready to start in the Start function.

Attach this script to our character and hit Play. Double-click at the debug line at the bottom and look at what we got—I'm awake printed before I'm ready to start as planned:

Your console messages should be similar to those displayed on the following screenshot:

Remember, there is no order in which the engine calls the Awake or Start functions among the objects by default, it can randomly choose one or another and call it from there. Another interesting thing is that the Start() function won't be called if an object is disabled. In other words, if we disable an object in the Awake() function, we can save some performance for our game to run faster at start-up.

Using specific of these functions we should be prepared to use Awake() for referencing objects, scripts, variables etc. Assigning default properties and start-up functionality is better in the Start() function.

Animation component and playing speed

We will use this script to control speed of animations and movement speed for the character; therefore, we need to declare the following variables to control them:

In order for the object to play animations, we need to attach an animation component to our character. Select character and go to Component | Miscellaneous | Animation, as shown in the following screenshot:

Inside Animation Controller, click on a small circle, it will lead you to the Select AnimationClip window. Click on any of the available animations.

Under the Animations drop-down menu, increase the size to 4 and assign a unique animation to each Element.

Uncheck the Play Automatically box. We don't want Unity to play random animation for us; we will take care of it through the code:

All the animation manipulations will be done through Animation Controller. The first thing that we need to learn about animations is WrapMode. WrapMode controls the play of animation—or repeating, to be more precise. There are a number of repeating modes available in Unity. They are as follows:

Once: It plays the animation once and stops

Loop: It plays the animation over and over again until told to stop

Ping-pong: It plays the animation till the end, then reverses and plays it backwards

Default: It reads a default repeat mode set higher up

ClampForever: It will play the animation till the end and then continuously keeps playing its last frame

We can specify WrapMode for all animations by referencing just an animation component or an individual animation, by specifying a name in square brackets: animation.wrapMode = WrapMode.Loop; or animation["idle"].wrapMode = WrapMode.Loop;

To play an animation, we simply call the Play function with name of the animation.

Animation scripting

In this section, we will put information learned in the preceding section into action. Perform the following steps:

When script initializes, we need to set WrapMode to looping by default.

Jump can be performed from any height, therefore, we have no idea how long animation should be played for. ClampForever, a loop playing the last frame of the animation, will help us here.

CrossFade is used to blend in between animations. Blending is a very important aspect of animations, as it helps to create numerous transitions from one animation to another.

Imagine that there was no blending. Our character would be walking, then instantly changing animation to jumping, shooting, landing, and so on. That will look weird and hard-edged. If we want to make smooth transactions from one animation to another, from jumping to landing to walking, for instance, we will have to manually create numerous animations. Thankfully, Unity can blend in between animations for us, with the CrossFade function. CrossFade interpolates one basic animation into another, creating more complex and unique animations for our character to play. We can even specify a speed of fading by adding an extra float parameter, like the following one:

animation.CrossFade("jump", 0.3);

0.3 seconds is a default value.

We will now add this functionality to our jump, right after we checked if our character didn't reach climax:

This script goes after the first if statement, at the very top. To determine whether the character is moving or not, we used the MoveDirection vector from CH_Controller.

Now we are left to deal with different movements. Realistically, we don't want our character to move with exactly the same speed in all directions. We will assign different values to the Speed variable in the Controller script based on the direction in which the character is moving:

We did exactly the same thing to every direction movement. The only exception should be forward movement. That's where we will implement running. In theory, we will check isRunning from CH_Controller and rewrite the function for moving forward as follows:

Alerts & Offers

Series & Level

We understand your time is important. Uniquely amongst the major publishers, we seek to develop and publish the broadest range of learning and information products on each technology. Every Packt product delivers a specific learning pathway, broadly defined by the Series type. This structured approach enables you to select the pathway which best suits your knowledge level, learning style and task objectives.

Learning

As a new user, these step-by-step tutorial guides will give you all the practical skills necessary to become competent and efficient.

Beginner's Guide

Friendly, informal tutorials that provide a practical introduction using examples, activities, and challenges.

Essentials

Fast paced, concentrated introductions showing the quickest way to put the tool to work in the real world.

Cookbook

A collection of practical self-contained recipes that all users of the technology will find useful for building more powerful and reliable systems.

Blueprints

Guides you through the most common types of project you'll encounter, giving you end-to-end guidance on how to build your specific solution quickly and reliably.

Mastering

Take your skills to the next level with advanced tutorials that will give you confidence to master the tool's most powerful features.

Starting

Accessible to readers adopting the topic, these titles get you into the tool or technology so that you can become an effective user.

Progressing

Building on core skills you already have, these titles share solutions and expertise so you become a highly productive power user.