Physics-based random movement in Unity

Physics-based random movement in Unity

Part 7 โ€“ From idea to game: A beginner's guide to creating games with Unity!

ยท

11 min read

In the previous article, I figured out how to create colliders around the visible area of the main camera. This means that my level is as big as what the main camera can see. This also means that there is now an enclosed area in which GameObjects are free to move. And that is what is on the menu today!

Preparing for physics-based movement ๐ŸŸ

A few articles back, I added temporary movement to our fish. This was based on changing the position of the transform component on the fish. While this is one way of creating movement, I want to utilise physics and colliders and so I have to use another way.

My current Fish is not able to collide with the colliders. This is because it requires a Rigidbody component to do so:

In physics simulation, rigid bodies enable physics-based behaviour such as movement, gravity, and collision.

Adding one turned out to be easy. I simply selected the "Capsule" under the "Fish" GameObject, clicked "Add Component" and selected Rigidbody.

Visual representation of adding a Rigidbody component to a 3D GameObject in Unity editor.

I (temporarily) removed the FishMovement script from the Fish GameObject and pressed play. As you can see, the Fish sinks to the bottom until it hits the bottom collider, which I set up in the last article.

Visual demo of what happens if you press play, once the Rigidbody component is attached. The fish sinks to the bottom.

Implementing physics-based movement โœจ

Sometimes, especially when learning, it is good to take a step back and think before starting right away. So that is what I did for this feature. What do I need to make a fish move? Well, there is going to be a method to do the actual moving. Additionally, I will need a method that decides where the Fish is going to move. Perhaps I would also need a method that checks if the fish is even able to move there. Here is how I did it.

First, I removed the Fish GameObject. But, before you do so, make sure to take out the Capsule child object and rename it to Fish. I realised that at this stage, it does not make sense to have a separate parent GameObject. I might end up reverting this change later on in the series, but for now, there is no need, and it only overcomplicates things.

Next, make sure the FishMovement script is attached to the "new" Fish GameObject. Your GameObject should now look like this:

Screenshot of the Unity Editor with the Fish GameObject selected. It shows the Transform, Capsule (Mesh Filter), Mesh Renderer, Capsule Collider, Rigidbody and Fish Movement (Script) components.

Next, I removed all the current code from the FishMovement script and created the code below. As per usual, I will show the result first and then explain each part separately.

using UnityEngine;

/// <summary>
/// Moves the GameObject around randomly within the bounds of the camera's view using a Rigidbody component.
/// </summary>
public class FishMovement : MonoBehaviour
{
    // Movement
    [SerializeField] private float _movementSpeed = 5.0f;
    private Vector3 _targetPosition;
    private Vector3 _direction;

    // Components
    private Rigidbody _ridigbody;

    // Misc
    private float _width;

    /// <summary>
    /// Initializes the required components and variables for the fish movement behavior.
    /// Freezes the rotation of the rigidbody, sets the initial target position to the current position, and determines the width of the capsule collider.
    /// </summary>
    private void Awake()
    {
        // Initialize the rigidbody and freeze its rotation
        _ridigbody = GetComponent<Rigidbody>();
        _ridigbody.constraints = RigidbodyConstraints.FreezeRotation;

        // Set the initial target position to the current position
        _targetPosition = transform.position;

        // Determine the width of the capsule collider
        // Since it's rotated 90 degrees on the Z axis, the height represents the width
        _width = GetComponent<CapsuleCollider>().height;
    }

    /// <summary>
    /// Updates the direction of the fish towards the target position.
    /// If the fish has reached the target position, a new random target position is set.
    /// </summary>
    private void Update()
    {
        _direction = (_targetPosition - transform.position).normalized;
        if(Vector3.Distance(_targetPosition, transform.position) < 2.0f)
        {
            _targetPosition = GetRandomPointInView();
        }
    }

    /// <summary>
    ///  Applies force to the rigidbody to move the fish towards the target position.
    /// </summary>
    private void FixedUpdate()
    {
        _ridigbody.AddForce(_direction * _movementSpeed);
    }

    /// <summary>
    /// Returns a random point within the camera's view.
    /// </summary>
    /// <returns>A random point within the camera's view.</returns>
    private Vector3 GetRandomPointInView()
    {
        // Calculate half the width of the fish
        float halfWidth = _width / 2.0f;

        // Generate a random point within the camera's view
        return Camera.main.ScreenToWorldPoint(new Vector3(
            Random.Range(halfWidth, Screen.width - halfWidth),
            Random.Range(halfWidth, Screen.height - halfWidth),
            Random.Range(Camera.main.nearClipPlane, Camera.main.farClipPlane))
        );
    }
}

Field definitions

I will start at the top of the code and work my way down, which logically brings us to the field definitions first.

// Movement
[SerializeField] private float _movementSpeed = 5.0f;
private Vector3 _targetPosition;
private Vector3 _direction;

// Components
private Rigidbody _ridigbody;

// Misc
private float _width;

There is nothing new here (Compared to the last article) except for the [SerializeField] and the Rigidbody. What do they do? Well according to the Unity Docs:

When Unity serializes your scripts, it only serializes public fields. If you also want Unity to serialize your private fields you can add the SerializeField attribute to those fields.

Serializing, in this context refers to making the fields visible and editable in the Unity Editor, while they normally would not be since they are private. This makes it easier to quickly change values and test what works best. So, while I set the default movement speed to 5.0f for now, I can change it in the editor, before pressing play, or during a test run.

Next up, is the Rigidbody. Which, according to Unity Docs, controls the object its position through physics simulation. This component is required to apply force, to make it move around. To do so, I need access to the component in the script and that requires defining a field.

The rest of the fields are ones I have already covered, such as Vector3 and Float. As you can see, I have grouped them with double slashed (//) comments. This is overkill if you only declare 5 fields, but who knows what will be added along the way.

Awake()

Then you will see the first method, Awake():

/// <summary>
/// Initializes the required components and variables for the fish movement behavior.
/// Freezes the rotation of the rigidbody, sets the initial target position to the current position, and determines the width of the capsule collider.
/// </summary>
private void Awake()
{
    // Initialize the rigidbody and freeze its rotation
    _ridigbody = GetComponent<Rigidbody>();
    _ridigbody.constraints = RigidbodyConstraints.FreezeRotation;

    // Set the initial target position to the current position
    _targetPosition = transform.position;

    // Determine the width of the capsule collider
    // Since it's rotated 90 degrees on the Z axis, the height represents the width
    _width = GetComponent<CapsuleCollider>().height;
}

For those paying attention, this is different than the Start() method I used in the last article. But how? And why? This answer on StackOverflow by Minzkraut explains it clearly. I suggest you read his answer, but to summarise, Start() may not be called on the same frame as Awake() if the script is not enabled at initialisation time.

In this use case, it does not make a difference yet. And as always, perhaps I will revert the change later on if it continues to not have one. Remember, I am also still learning ๐Ÿค“.

So, in the method itself. What do I do there? First, I set the Rigidbody component to the earlier defined field, and then add rotation constraints. You probably see it coming from miles away, but here is what the Unity Docs say about RigidBody.constraints:

Controls which degrees of freedom are allowed for the simulation of this Rigidbody.

With constraints to the Rigidbody you can freeze or lock the movement and/or rotation altogether or on a per-axis basis. As I do not want the fish to rotate at this point, I simply set the _rigidbody.constraints to RigidbodyConstraints.FreezeRotation.

Additionally, I also set the _targetPosition, the position the fish is supposed to move to, to the actual location of the fish. Once Update() is called once, it will set it to a random location, but more on that later.

Finally, I calculate the width of the Fish in Awake(). This is what I will use later, to prevent the Fish to try and get to a location it can not get to. One small sidenote, which I also mention in the comment above that line, is that we call .height on the CapsuleCollider, because we have rotated the capsule by 90 degrees on the Z axis.

Update()

Onto the next method, Update().

/// <summary>
/// Updates the direction of the fish towards the target position.
/// If the fish has reached the target position, a new random target position is set.
/// </summary>
private void Update()
{
    _direction = (_targetPosition - transform.position).normalized;
    if(Vector3.Distance(_targetPosition, transform.position) < 2.0f)
    {
        _targetPosition = GetRandomPointInView();
    }
}

The Update() method in this script is quite small for now. The only thing I do here is set _direction, and call GetRandomPointInView() to then set the returned value to _targetPosition, if the Fish is close enough to the target.

First, the direction. This line of code calculates the direction from the fish its current position to its target position and then normalizes this direction vector. The normalized vector represents the direction that the fish needs to move to reach its target position.

Then, by using Vector3.Distance() I calculate the distance between the current and target position. If it is smaller than 2.0f, we update the _targetPosition to a new value, by calling GetRandomPointInView() and save the result to it. I will explain that method in a bit, but first, FixedUpdate().

FixedUpdate()

/// <summary>
///  Applies force to the rigidbody to move the fish towards the target position.
/// </summary>
private void FixedUpdate()
{
    _ridigbody.AddForce(_direction * _movementSpeed);
}

Wait what? First, you moved from Start() to Awake() and now from Update() to FixedUpdate()? Well, yes and no. In this script, I use both updates. Looking at the, you guessed it, Unity Docs, there is quite some explanation there.

I highly suggest reading that documentation but in short, Update() is called every frame, while FixedUpdate() is, as the name implies, called in fixed intervals. Which, by the way, can be set via Edit > Settings > Time > Fixed Timestep. But what does this mean? Well, imagine the following example.

Imagine we would apply force in an update method. Player A is playing on a computer that has 25 FPS (Frame per second). They would have the force applied 25 times per frame. However, player B, who has a more powerful system, running at 100 FPS will have 4x the number of updates called. This will create a wildly varying experience for both players.

Therefore, as the documentation explains

Use FixedUpdate when using Rigidbody. Set a force to a Rigidbody and it applies each fixed frame. FixedUpdate occurs at a measured time step that typically does not coincide with MonoBehaviour.Update.

So what did I do with the method? For now, it is quite simple. We add force to the Rigibody, and the value is the _direction * _movementSpeed.

GetRandomPointInView()

Then the final method of this script, GetRandomPointInView():

/// <summary>
/// Returns a random point within the camera's view.
/// </summary>
/// <returns>A random point within the camera's view.</returns>
private Vector3 GetRandomPointInView()
{
    // Calculate half the width of the fish
    float halfWidth = _width / 2.0f;

    // Generate a random point within the camera's view
    return Camera.main.ScreenToWorldPoint(new Vector3(
        Random.Range(halfWidth, Screen.width - halfWidth),
        Random.Range(halfWidth, Screen.height - halfWidth),
        Random.Range(Camera.main.nearClipPlane, Camera.main.farClipPlane))
    );
}

This is how I pick a random point within view. You can see that the return type of the method is Vector3, which is how I was able to set the returned value of this method to a field earlier.

The first thing I do in this method is calculate half of the width of the Fish. I do this to use that value as an offset of the borders. The reason for this is that I check if the Fish is close enough to the _targetPosition, and if so, pick a new position to go to. If the _targetPosition is exactly at the border, it could occur that the Fish is never able to reach it. The centre point of the Fish is where its position is, but it also has a width and there are colliders. So to prevent this from happening I calculate half the width of the Fish, which I then take into consideration when picking a point in view.

Next, I use return followed by a method to calculate a random world point and then return it. Of course, ScreenToWorldPoint() will return a Vector3, because the return type of the method is Vector3. So what do the Unity Docs say about this method?

Transforms a point from screen space into world space, where world space is defined as the coordinate system at the very top of your game's hierarchy.

So in essence I am using Random.Range() for each axis to pick a random point based on the width and height of the screen. For the Z-axis (Depth), I use the nearClipPlane and farClipPlane, which I highlighted in the last article.

Lo and behold ๐Ÿคฉ

At this stage, I have prepared and changed the GameObject, and created quite some code, and perhaps it has got you wondering, what does all this work do?

Well, it moves the GameObject around randomly:

Showing the random movement in action, where a capsule is moving around in the playable area, after pressing play in the Unity Editor.

The way a fish moves differs per type of fish. But let us be honest, no matter what type you choose, the movement in the gif above does not look very fish-like yet. So that is something I will surely iterate on in a later article.

Congratulations ๐ŸŽ‰

There is random movement! And while the way it moves does not look like a real fish yet, I am confident that by iterating on what I created today, it will get there.

As always, if you have any questions or issues following along, or have feedback on the article or my writing, please do let me know ๐Ÿ™Œ!

Did you find this article valuable?

Support BFranse by becoming a sponsor. Any amount is appreciated!

ย