© Casey Hardman  2020
C. HardmanGame Programming with Unity and C#https://doi.org/10.1007/978-1-4842-5656-5_37

37. Pulling and Pushing

Casey Hardman1 
(1)
West Palm Beach, FL, USA
 

In this chapter, we’ll make use of raycasting to allow the player to point their camera at a GameObject with a Rigidbody and hold left-click to pull the object toward them or right-click to push it away from them. Rather than pulling and pushing by moving the Transform directly, we’ll apply force to the Rigidbody so that the physics system handles the motion instead for us.

Script Setup

The pushing and pulling features will be implemented in a separate “Telekinesis” script that we’ll attach to the Player GameObject. We’ll do this instead of writing everything in the Player script, just to keep things organized.

Start off by creating a script named Telekinesis in the Scripts folder of your project. Open it up, and let’s declare our variables:
public class Telekinesis : MonoBehaviour
{
    public enum State
    {
        Idle,
        Pushing,
        Pulling
    }
    private State state = State.Idle;
    [Header("References")]
    public Transform baseTrans;
    public Camera cam;
    [Header("Stats")]
    [Tooltip("Force applied when pulling a target.")]
    public float pullForce = 60;
    [Tooltip("Force applied when pushing a target.")]
    public float pushForce = 60;
    [Tooltip("Maximum distance from the player that a telekinesis target can be.")]
    public float range = 70;
    [Tooltip("Layer mask for objects that can be pulled and pushed.")]
    public LayerMask detectionLayerMask;
    //Current target of telekinesis, if any.
    private Transform target;
    //The world position that the target detection ray hit on the current target.
    private Vector3 targetHitPoint;
    //Rigidbody component of target.  For something to be marked as a target, it must have a Rigidbody.
    //So as long as 'target' is not null, this won't be null either.
    private Rigidbody targetRigidbody;
    //If there is no current target, this is always false.  Otherwise, true if the target is in range, false if they are not.
    private bool targetIsOutsideRange = false;
    //Gets the Color that the cursor should display based on the state and target distance.
    private Color CursorColor
    {
        get
        {
            if (state == State.Idle)
            {
                //If there is no target, return gray:
                if (target == null)
                    return Color.gray;
                //If there is a target but it's not in range, return orange:
                else if (targetIsOutsideRange)
                    return new Color(1,.6f,0);
                //If there is a target and it is in range, return white:
                else
                    return Color.white;
            }
            //If we're pushing or pulling, return green:
            else
                return Color.green;
        }
    }
}

Our variables explain themselves in their tooltips and comments, so let’s go over how the system works and where the variables find their purpose.

Every frame, we’ll cast a ray using the “detectionLayerMask” with infinite distance (range). If a valid target is found with the ray, we’ll set the related variables:
  • target

  • targetHitPoint

  • targetRigidbody

  • targetIsOutsideRange

This gives us all we need to know about our target, if we have one. As you can see, we still target objects that are outside of our “range” variable, but we have the “targetIsOutsideRange” bool to tell us if the target can actually be pulled or pushed.

We then check for input: holding the left mouse button while we have a valid, in-range target will pull the target toward us, while right-click will push the target away.

We’ll set our “state” based on what we were doing on this frame: nothing (Idle), pushing, or pulling.

Our CursorColor property reacts to the “state” as well as whether we have a target that is in range, returning a different Color based on these factors:
  • If there is no target, CursorColor returns gray.

  • If there is a target but it is outside the range, CursorColor returns orange.

  • If there is a target and it is inside range, CursorColor returns white.

  • While we are pushing or pulling a target, CursorColor returns green.

The “cursor” is a small dot we’ll draw in the center of the screen, where the raycast originates. Of course, this dot will use CursorColor to define its color. This will make it automatically update based on the situation, to give the player some indication of when they have a valid target, when the target is outside range, and when they’re actively pulling or pushing their target.

Before we continue, let’s set up the Telekinesis script. First, add an instance of the script to your root Player GameObject (the same one that has the Player script instance). Set the “baseTrans” reference to the Player Transform and drag and drop the Player Camera onto the “cam” reference field. Make sure to also set the layer mask to include only the Default layer. When you’re done, your script should look something like Figure 37-1 in the Inspector.
../images/486227_1_En_37_Chapter/486227_1_En_37_Fig1_HTML.jpg
Figure 37-1

Our Telekinesis script with all of its fields correctly set in the Inspector

Moving on, let’s map out our basic functionality with some methods:
//Update logic:
void TargetDetection()
{
}
//FixedUpdate logic:
void PullingAndPushing()
{
}
//Unity events:
void Update()
{
    TargetDetection();
}
void FixedUpdate()
{
    PullingAndPushing();
}

You’ll notice we’re using a new built-in Unity event here: FixedUpdate. This is where the actual pulling and pushing will be performed, while the raycast for target detection will instead occur in the normal Update event that we’re so used to.

FixedUpdate

The FixedUpdate method is like Update, but you should use FixedUpdate instead if you intend on interacting with the physics system through code. Notably, applying forces to Rigidbodies should be done through FixedUpdate instead of Update. It occurs not once per frame, but at a set interval with the same amount of time between each FixedUpdate. Unity warns that unexpected results can occur in physics components if you interact with them in Update instead of FixedUpdate!

How frequently the physics updates are called is dependent on a value that you can set by navigating to the Edit ➤ Project Settings window, clicking the Time tab, and locating the Fixed Timestep value, shown in Figure 37-2. By default, the value is set to .02, which means FixedUpdates are called 50 times per second.
../images/486227_1_En_37_Chapter/486227_1_En_37_Fig2_HTML.jpg
Figure 37-2

The Time tab is open in the Project Settings window, where the Fixed Timestep field is at the top of the listings

Setting the value lower will generate more updates per second but comes at the cost of performance. Of course, setting it higher will save on performance but may make physics less accurate or downright choppy at particularly high values.

Within a FixedUpdate call, Time.deltaTime will still work the same way, returning the Fixed Timestep value always. You can also access Time.fixedDeltaTime to get this value within your code. You can even set it in-game to dynamically change the update frequency of physics.

Target Detection

Before coding our FixedUpdate logic, let’s get our target detection working so we know what we’re dealing with.

We’ll fill in the TargetDetection method we declared before with this code:
void TargetDetection()
{
    //Get a ray going out of the center of the screen:
    var ray = cam.ViewportPointToRay(new Vector3(.5f,.5f,0));
    RaycastHit hit;
    //Cast the ray using detectionLayerMask:
    if (Physics.Raycast(ray,out hit,Mathf.Infinity,detectionLayerMask.value))
    {
        //If the ray hit something,
        if (hit.rigidbody != null && !hit.rigidbody.isKinematic) // and it has a non-kinematic Rigidbody,
        {
            //Set the telekinesis target:
            target = hit.transform;
            targetRigidbody = hit.rigidbody;
            targetHitPoint = hit.point;
            //Based on distance, set targetIsOutsideRange:
            if (Vector3.Distance(baseTrans.position,hit.point) > range)
                targetIsOutsideRange = true;
            else
                targetIsOutsideRange = false;
        }
        //If the thing the ray hit has no Rigidbody
        else
            ClearTarget();
    }
    else //If the ray didn't hit anything
        ClearTarget();
}

Here, we exhibit usage of the Camera.ViewportPointToRay method. This method is just like the ScreenPointToRay method that we used in our second project to detect where to place our tower building highlighter. The only difference is that it operates by the “viewport” instead of by a pixel position on the screen. It’s just a different way to locate a position on the camera view. Rather than specifying pixels, such as half of the width and height of the screen, we specify a fraction between 0 and 1 for the X and Y values. The X is left and right, and the Y is up and down, just like with pixels, but we don’t have to concern ourselves with the screen width and height. (0, 0) is the bottom-left corner of the camera, and (1, 1) is the top-right corner. Thus, (.5f, .5f) will get us the center. Since we don’t have to plug the mouse position into the method, this one will suit us just fine as an easy way to get a ray shooting out of the center of the screen.

The Z axis doesn’t do anything, so we just leave it at 0.

We cast the ray. If the ray hit anything, the “hit” will be filled with data about what was hit, as we’ve come to understand about raycasting.

We’ll only mark something as a target if it has a Rigidbody and only if that Rigidbody is not kinematic. You’ll recall that a kinematic Rigidbody is not controlled by the physics system. We can’t apply forces to such a Rigidbody anyway, so they don’t make for valid targets.

We set our four target-related variables for future reference.

If the target did not have a non-kinematic Rigidbody, or if the ray simply didn’t hit anything in the first place, we call a ClearTarget method.

Let’s declare that method. It’s a simple one that just resets the values of the variables to null and false. I’ll put it down below the CursorColor property:
void ClearTarget()
{
    //Clear and reset variables that relate to targeting:
    target = null;
    targetRigidbody = null;
    targetIsOutsideRange = false;
}

That does it for target detection. We can now expect our camera to constantly be shooting a ray out of its center, striking only the layers defined in our “detectionLayerMask”. It will detect and store information about the target the ray strikes, if any. Otherwise, it clears the target.

Pulling and Pushing

Now we can fill in the method that does the interesting part: detecting mouse buttons and applying forces to pull or push the target.

We’ll fill in the PullingAndPushing method with this code:
void PullingAndPushing()
{
    //If we have a target that is within range:
    if (target != null && !targetIsOutsideRange)
    {
        //If the left mouse button is down
        if (Input.GetMouseButton(0))
        {
            //Pull the target from the hit point towards our position:
            targetRigidbody.AddForce((baseTrans.position - targetHitPoint).normalized * pullForce,ForceMode.Acceleration);
            state = State.Pulling;
        }
        //Else if the right mouse button is down
        else if (Input.GetMouseButton(1))
        {
            //Push the target from our position towards the hit point:
            targetRigidbody.AddForce((targetHitPoint - baseTrans.position).normalized * pushForce,ForceMode.Acceleration);
            state = State.Pushing;
        }
        //If neither mouse buttons are held down
        else
            state = State.Idle;
    }
    //If we don't have a target or we have one but it is not in range:
    else
        state = State.Idle;
}

The target Rigidbody is accessed so we can call its AddForce method. This method takes a Vector3 for the amount of force to apply, as well as a ForceMode enum that defines how the force applies to the Rigidbody.

To apply the force, we use that familiar equation to get the direction we desire:
(to - from).normalized

Then we multiply that direction by the force we want to apply, either “pullForce” or “pushForce” based on which one we’re doing.

The ForceMode has four values that change two factors of how the force is applied:
  • Does it happen as a constant push, like one object pressing against another, or as a sudden impact, like an explosion?

  • Is it affected by the mass of the Rigidbody?

Those four possible values are
  • Force, which is a constant push that is affected by mass

  • Acceleration, which is a constant push that ignores mass

  • Impulse, which is a sudden push that is affected by mass

  • VelocityChange, which is a sudden push that ignores mass

Our selection, Acceleration, ensures that the force we apply is not going to be an instant impulse, as if the object was being hit by a wave of force from an explosion or something of the sort. It is more like a gradual, constant influence pulling it toward us.

It is also ignoring the mass, which means if we pull or push a Rigidbody with a very high mass, the force will still affect the Rigidbody just as much. This makes it so you can make heavy objects and still allow the player to pull and push them.

Aside from applying the force, we also manage the “state” enum so that it always reflects what we were doing during the last FixedUpdate call.

With that in place, we can now pull and push Rigidbodies, but we still need to draw our cursor.

Cursor Drawing

We’ll make a basic four-pixel (two-by-two) square in the center of the screen that gives the player indication of where their telekinesis ray is being cast from, with a color that responds to the situation.

To do this, all we need is one line of code in an OnGUI event method. As you may remember, we can call GUI methods from the built-in OnGUI event to draw 2D user interface elements to the screen. In our situation, this will be a quick and easy way to draw a simple swatch of color to the screen through code, rather than setting up a Canvas with a UI element for our cursor.

We’ll write our OnGUI method beneath the FixedUpdate method:
void OnGUI()
{
    //Draw a 2x2 rectangle of the CursorColor at the center of the screen:
    UnityEditor.EditorGUI.DrawRect(new Rect(Screen.width * .5f,Screen.height * .5f,2,2),CursorColor);
}

We’re reaching into the UnityEditor namespace to access this method because it’s only available through “EditorGUI,” not the normal “GUI”. You could put a “using UnityEditor;” line at the very top of the script file and cut out the “UnityEditor” part of the reference, if you want, but since we’re only using one UnityEditor reference in the script, it won’t save us much typing.

One thing to note is that you won’t be able to build a game if you’re running EditorGUI methods in your game code. The methods are really only meant for use in the Unity editor, which is fine for our purposes. If you were coding for a real game instead of just testing features like we are, you would want to implement the cursor with an actual UI element, like a Panel, and you’d change its color through a reference.

Moving on, the method we’re calling is a basic one that just draws a rectangle with a given solid color. It takes a Rect as its first parameter and the Color as the second.

You may remember our usage of the Rect data type (short for rectangle) from our first project:
  • The first parameter is the X position of the left side of the rectangle.

  • The second parameter is the Y position of the top side of the rectangle.

  • The third parameter is the width.

  • The fourth parameter is the height.

A value of 0 in the X position is the left edge of the screen, while a value of “Screen.width” would put it all the way at the right edge of the screen.

Similarly, 0 for the Y axis is the bottom edge, while “Screen.height” is the top.

We simply put our rectangle right in the center of the screen by using half of the screen width and height as its position.

The rectangle is not filled in by default – it’s just a rectangular outline, a 1-pixel-thin border. However, if we make it only 2 pixels wide and 2 pixels tall, it will show as a 2×2 square – four pixels in total, all pressed up against each other. It’s small, but we don’t want it to get in the way of what you’re trying to point at anyway, so it will do.

Once this code is in, you’ll be able to see where that ray is coming out of your screen.

To test out the Telekinesis features, try creating three cubes on the ground near the player. Give each one a Rigidbody and give each one a higher mass than the last one. You can make the scale match the mass too, if you want – make the second cube have a scale of (2, 2, 2) and a mass of 2, for example. Then, point at them with the center of your camera and try to pull (left-click) and push (right-click). You’ll see how the Rigidbody takes over the physics, causing the object to turn and bounce as it moves.

Summary

This chapter taught us how to apply external forces to Rigidbodies using the AddForce method, as well as the four different options for applying force that Unity provides to us. We also learned that Unity’s physics simulations occur at a fixed timestep, not “once per frame,” and any code that interacts with the physics system constantly should occur in a FixedUpdate event, not Update.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.152.162