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.
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.
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.
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.
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!
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.
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.
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.
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.
Then we multiply that direction by the force we want to apply, either “pullForce” or “pushForce” based on which one we’re doing.
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?
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’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.
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.