I'm trying to create a smooth camera movement in 2D. The target I want to hit can potentially move a large distance in a single frame, and is not like a character moving smoothly from A to B.
I'm aware of possible solutions like using Vector2.Lerp(), but that approach only slows down nicely but speeds up abruptly.
_position = Vector2.Lerp(_position, target, 0.5f * Time.deltaTime);
I've tried implementing the "arrive" steering behaviour, but cannot make it work nicely together with acceleration - especially when the target is close to the current position.
I managed to make it work pretty well in one axis, but that approach didn't work when repeated in a second axis.
var decelerateRadius = GetDistanceFromAcceleration(acceleration, Mathf.Abs(_velocity));
var direction = target - _position;
var distance = Mathf.Abs(direction);
var a = acceleration * Time.deltaTime;
if (distance > 0.0005f)
{
if (distance < decelerateRadius.x)
{
_velocity *= distance / decelerateRadius.x;
}
else
{
_velocity += direction.x >= 0.0f ? a : -a;
}
}
else
{
_velocity = 0.0f;
}
// move tracker
_position += _velocity * Time.deltaTime;
And my method for calculating the distance based on acceleration:
private Vector2 GetDistanceFromAcceleration(float a, float vx, float vy)
{
// derived from: a = (vf^2 - vi^2) / (2 * d)
return new Vector2(vx * vx / (2.0f * a), vy * vy / (2.0f * a));
}
My last attempt was making a rolling average of the target, but it suffered the same issue as lerping.
To summarize the requirements:
Must accelerate
Must decelerate and stop at target
Must not "orbit" or in other ways swing around the target, before stopping
Target must be able to move
May be limited by a maximum velocity
Any tips, pointers og solutions on how to achieve this?
I've also asked the question over at game dev
https://gamedev.stackexchange.com/questions/170056/accelerate-decelerate-towards-moving-target-and-hitting-it
The problem with your lerp is also that you actually never reach the target position you just get very very close and small.
I thought about something like this
as long as you are already at the targets position don't move. Enable orbit mode
while not within a certain targetRadius around the target position accelerate from to maxVelocity
if getting within a certain targetRadius around the target position decelerate depending on the distance / radius will be a value between 1 and 0
To get the distance there is already Vector2.Distance you could/should use.
For the movement I would recommend Vector2.MoveTowards which also avoids overshooting of the target.
something like
public class SmoothFollow2D : MonoBehaviour
{
[Header("Components")]
[Tooltip("The target this will follow")]
[SerializeField] private Transform target;
[Header("Settings")]
[Tooltip("This may never be 0!")]
[SerializeField] private float minVelocity = 0.1f;
[SerializeField] private float maxVelocity = 5.0f;
[Tooltip("The deceleration radius around the target.\nThis may never be 0!")]
[SerializeField] private float targetRadius = 1.0f;
[Tooltip("How much speed shall be added per second?\n" +
"If this is equal to MaxVelocity you know that it will take 1 second to accelerate from 0 to MaxVelocity.\n" +
"Should not be 0")]
[SerializeField] private float accelerationFactor = 3.0f;
private float _currentVelocity;
private float _lastVelocityOutsideTargetRadius;
private bool _enableOrbit;
public bool EnableOrbit
{
get { return _enableOrbit; }
private set
{
// if already the same value do nothing
if (_enableOrbit == value) return;
_enableOrbit = value;
// Whatever shall be done if orbit mode is enabled or disabled
}
}
private void Update()
{
if (target == null) return;
var distanceToTarget = Vector2.Distance(transform.position, target.position);
// This is the threshold Unity uses for equality of vectors (==)
// you might want to change it to a bigger value in order to
// make the Camera more stable e.g.
if (distanceToTarget <= 0.00001f)
{
EnableOrbit = true;
// do nothing else
return;
}
EnableOrbit = false;
if (distanceToTarget <= targetRadius)
{
// decelerate
// This will make it slower
// the closer we get to the target position
_currentVelocity = _lastVelocityOutsideTargetRadius * (distanceToTarget / targetRadius);
// as long as it is not in the final position
// it should always keep a minimum speed
_currentVelocity = Mathf.Max(_currentVelocity, minVelocity);
}
else
{
// accelerate
_currentVelocity += accelerationFactor * Time.deltaTime;
// Limit to MaxVelocity
_currentVelocity = Mathf.Min(_currentVelocity, maxVelocity);
_lastVelocityOutsideTargetRadius = _currentVelocity;
}
transform.position = Vector2.MoveTowards(transform.position, target.position, _currentVelocity * Time.deltaTime);
}
// Just for visualizing the decelerate radius around the target
private void OnDrawGizmos()
{
if (target) Gizmos.DrawWireSphere(target.position, targetRadius);
}
}
The MinVelocity is actually necessary for the edge case when the target is moved not further than TargetRadius and lastVelocityOutsideTargetRadius si still 0. In that case no acceleration takes place so lastVelocityOutsideTargetRadius is never updated.
With the values you have to play a bit ofcourse ;)
It might not be perfect yet but I hope it is a good start point to develop that further (only looks laggy due to 15 FPS for the Gif ;) )
Related
I should say that I have very very little C# and Unity experience. I got most of this code from a Brackeys tutorial.
So, this code originally just made the player move at a set speed. So, I added a new public float set to a higher speed and set an if statement to change the value of the float that controls the speed (the float is literally named speed). I added a Debug.Log to display if the if statement is triggered/what the value of the speed float is. This log entry always displays once the Lshift key is pressed (default control axes have Fire3 set to Lshift inside of the unity project settings). I think my issue is that my speed is somehow decreasing before the player even moves. Additionally, this is probably the worst way to implement a sprint function, so if anyone has any better ideas, please let me know!
Clarification: The player can move. My intended effect is for the sprint key (Lshift) to be held down to achieve the sprinting effect. I did intend for the speed to reset every frame, so that as soon as the Lshift key is let go, the speed of the player will drop down to the normal state (12f). Currently I have a Debug.Log in place that displays the value of the "speed" float whenever the Lshift key is pressed. This always returns 18 in the console, which is the intended effect. However, even though this number is displayed in the console, there is no actual movement speed change in the player.
public CharacterController controller;
public float speed = 16f;
public float gravity = -9.81f;
public float jumpHeight = 3f;
public float sprintspeed = 18f;
public Transform groundCheck; //this is where the groundcheck empty object goes
public float groundDistance = 0.4f;
public LayerMask groundMask; //make sure to add a layer into the unity project called "ground"
Vector3 velocity;
bool isGrounded;
void Update()
{
isGrounded = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask); //this just creates a sphere and checks it's collision state with any other objects
if(isGrounded && velocity.y < 0)
{
velocity.y = -2f; //sets velocity to a low static number.
}
float x = Input.GetAxis("Horizontal");
float z = Input.GetAxis("Vertical");
speed = 12f;
if(Input.GetButtonDown("Fire3")) //theoretically increases the speed float only when left control is pressed down
{
speed = sprintspeed;
Debug.Log(speed);
}
Vector3 move = transform.right * x + transform.forward * z;
controller.Move(move * speed * Time.deltaTime);
if(Input.GetButtonDown("Jump") && isGrounded)//jump function below
{
velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
}
velocity.y += gravity * Time.deltaTime;
controller.Move(velocity * Time.deltaTime);
}
Thanks in advance!
I would look to see what values are in the inspector since both speed and sprintspeed are public. There is a chance that sprintspeed might be set to 12f and therefore not change the speed. Also, keep in mind that with your implementation you can't change the speed variable from the inspector since you're updating it back to 12f every frame.
If that doesn't help with anything, perhaps some clarification is needed. Is your player moving at all? Is it just the sprinting that isn't working? What value does your Debug.Log give you? What are the values in the inspector?
Here is a simple script to demonstrate the issue. This script should move the cursor to the left for 1 second and then back to the right for 1 second. However you'll see it just moves it to the left then stops. You can flip the order and try to move right first, but you'll notice that still won't work. I've also tested with moving up / down, moving up works as expected, moving down does not.
public class dumbtestscript : MonoBehaviour
{
void Start()
{
Mouse.current.WarpCursorPosition(new Vector3(123, 123));
StartCoroutine(MoveCursorLeftThenRightCoroutine());
}
IEnumerator MoveCursorLeftThenRightCoroutine()
{
float startTime = Time.time;
while (Time.time < startTime + 1)
{
Mouse.current.WarpCursorPosition(Input.mousePosition +
(Time.deltaTime * new Vector3(-0.1f, 0)));
yield return null;
}
while (Time.time < startTime + 2)
{
Mouse.current.WarpCursorPosition(Input.mousePosition +
(Time.deltaTime * new Vector3(0.1f, 0)));
yield return null;
}
}
}
Am I misunderstanding something about how WarpCursorPosition is supposed to work?
Any advice appreciated. Thanks.
Edit: Upon further inspection it may have to do with the vector being passed to WarpCursorPosition. For example regardless of whether we use this to move left:
Mouse.current.WarpCursorPosition(Input.mousePosition + new Vector3(-0.1f, 0));
or this:
Mouse.current.WarpCursorPosition(Input.mousePosition + new Vector3(-1f, 0));
it moves to the left at the same speed. So it seems anything between -0.1 and -1 is being rounded to -1. Conversely for going right everything between 0.1 and just under 1 is being rounded to 0 which would explain why it wasn't moving in the original.
So everything is getting round down to the nearest integer for some reason? Both Input.mousePosition and the Vector3 we are adding are both Vector3 and I thought could handle float numbers so not sure why things are being rounded down to ints, if thats whats happening?
The issue
So everything is getting round down to the nearest integer for some reason?
Most probably Yes!
WarpCursorPosition makes your actual system cursor jump to the given pixel coordinates.
While inside Unity the pixel space is always provided as float for many things and mainly for making direct calculations with it, the actual system cursor uses pixels (like everything that is actually happening on the screen) which are always full int values!
So, yes, it will always (not round but rather) floor your given input to the next int (full pixel). Basically what happens if you simply use
var x = (int) (Input.mousePosition.x - 0.1f);
=> You will see that x will be exactly Input.mousePosition.x - 1 since if e.g. the current Input.mousePosition.x was 230 then you get (int) 229.9f which is 229.
In the other direction though you get e.g. 230 + 0.1f so (int) 230.1f again simply is 230.
=> Your steps would need to at least be one full pixel!
Solution
So instead of constantly read the current Input.mousePosition which underlies the afore mentioned pixel coordinates rather use and update a local vector that does not:
IEnumerator MoveCursorLeftThenRightCoroutine()
{
// Store the current mousePosition as a Vector2
// This uses floats and is not restricted to full pixel jumps
var currentPosition = Input.mousePosition;
for(var passedTime = 0f; passedTime < 1; passedTime += Time.deltaTime)
{
// simply continously add to the float vector
currentPosition += Vector2.left * speedInPixelsPerSecond * Time.deltaTime;
// Using that float vector as input may in some frames cause no movement where the clamp results
// still in the same pixel but it will catch up as soon as the vector was increased/reduced enough
Mouse.current.WarpCursorPosition(currentPosition);
yield return nnull;
}
for(var passedTime = 0f; passedTime < 1; passedTime += Time.deltaTime)
{
currentPosition += Vector2.right * speedInPixelsPerSecond * Time.deltaTime;
Mouse.current.WarpCursorPosition(currentPosition);
yield return null;
}
}
In general for the new Input system I would rather use Mouse.current.position instead of Input.mousePosition.
I don't have any experience using WarpCursorPosition but when I was experimenting to answer this thread, I felt it's not consistent & would like to advice not to use it.
Code
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
public class Q69189325 : MonoBehaviour
{
private void Start()
{
Mouse.current.WarpCursorPosition(Input.mousePosition);
StartCoroutine(MoveLogicCrt());
}
IEnumerator MoveLogicCrt()
{
yield return MoveCursorToDirectionCrt(Vector3.left, 150, 5);
yield return null;
yield return MoveCursorToDirectionCrt(Vector3.right, 350, 5);
}
private IEnumerator MoveCursorToDirectionCrt(Vector3 direction, float speed, float movementTime)
{
float startTime = Time.time;
while(Time.time < startTime + movementTime)
{
Vector2 newMousePosition = Input.mousePosition + (Time.deltaTime * direction * speed);
Mouse.current.WarpCursorPosition(newMousePosition);
yield return null;
}
}
}
Note:
For some reason when I put the same speed to both left & right it doesn't move right which was weird.
Execution Overview
https://gfycat.com/eventhriftyannelida
Happy coding!
I used from this code but it does not do it:
public Transform[] points;
public float speed;
private void OnTriggerEnter2D(Collider2D other)
{
if (other.tag == "door1")
{
transform.position = Vector3.Lerp(transform.position, points[1].position, speed * Time.deltaTime);
}
}
That is, I want the pig to go to a higher point on the ground at a desired speed when it hits the trigger (see the photo attached to see it)
Two problems here:
you are calling that line exactly once when you enter the collider so the movement is applied for I single frame!
Lerp interpolates linear between two positions using a factor between 0 and 1. You every time use the current position as start point so what would happen if if you called this continuously is approximating the position getting slower and slower every frame which is not what you describe. You want to move with a constant speed.
You most likely would rather use a Coroutine and MoveTowards for that
private void OnTriggerEnter2D(Collider2D other)
{
// Better use CompareTag here
if (other.CompareTag("door1"))
{
// Start a routine for the continuous movement
StartCoroutine(MoveTo(points[1].position, speed);
}
}
private IEnumerator MoveTo(Vector3 targetPosition, float linearSpeed)
{
// This uses an approximation of 0.00001 for equality
while(transform.position != targetPosition)
{
// with a constant speed of linearSpeed Units / second
// move towards the target position without overshooting
transform.position = Vector3.MoveTowards(transform.position, targetPosition, linearSpeed * Time.deltaTime);
// Tell Unity to "pause" the routine here, render this frame
// and continue from here in the next frame
yield return null;
}
// to be sure to end up with exact values set the target position fix when done
transform.position = targetPosition;
}
Alternatively a bit more complex looking but more powerful would be to rather calculate the required time depending on the speed but still adding some smoothing like e.g.
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("door1"))
{
StartCoroutine (MoveTo(points[1].position, speed);
}
}
private IEnumerator MoveTo(Vector3 targetPosition, float averageSpeed)
{
// store the initial position
var from = transform.position;
// Get the expected duration depending on distance and speed
var duration = Vector3.Distance(from, targetPosition) / averageSpeed;
// This is increased over time
var timePassed = 0;
while(timePassed < duration)
{
// This linear grows from 0 to 1
var factor = timePassed / duration;
// Adds some ease-in and ease-out at beginning and end of the movement
factor = Mathf.SmoothStep(0, 1, factor);
// linear interpolate on the smoothed factor
transform.position = Vector3.Lerp(from, targetPosition, factor);
// increase by time passed since last frame
timePassed += Time.deltaTime;
// Tell Unity to "pause" the routine here, render this frame
// and continue from here in the next frame
yield return null;
}
// to be sure to end up with exact values set the target position fix when done
transform.position = targetPosition;
}
OnTriggerEnter2D is only called once on every collider enter. Given you are only moving it by speed * Time.deltaTime with Time.deltaTime being in the order of 0.008 - 0.100 it may only move slightly.
Depending on what you want, are you sure you don't want to completely move the object or alternatively set a flag that starts moving it in the update() method?
right now I am trying to make rts camera zooming with panning when close to the ground. The problem I have now is that I use mouse scroll wheel for zooming and it makes the zooming feel like it's laggy. It looks like it jumps some Y value and teleports rather than smoothly move to the desired position. Also, I would like to know how to make the camera stop at the minimum Y value because what is happening now is that it stops at around 22 rather than 20 which is my minimum Y value for the camera movement.
I tried zooming with + and - on my numpad and it worked how I wanted (smoothly zooming in and out without skipping) but not all players have numpad and I feel it is more suitable to zoom with mouse wheel.
{
private const int levelArea = 100;
private const int scrollArea = 25;
private const int scrollSpeed = 25;
private const int dragSpeed = 70;
private const int zoomSpeed = 50;
// Maximum/minimum zoom distance from the ground
public int zoomMin = 20;
public int zoomMax = 120;
private const int panSpeed = 40;
// Minimal/maximal angles for camera
private const int panAngleMin = 30;
private const int panAngleMax = 90;
void Update()
{
// Init camera translation for this frame.
var translation = Vector3.zero;
// Zoom in or out
var zoomDelta = Input.GetAxis("Mouse ScrollWheel") * zoomSpeed * Time.deltaTime;
if (zoomDelta != 0)
{
translation -= Vector3.up * zoomSpeed * zoomDelta;
}
// Start panning camera if zooming in close to the ground or if just zooming out.
var pan = transform.eulerAngles.x - zoomDelta * panSpeed;
pan = Mathf.Clamp(pan, panAngleMin, panAngleMax);
// When to start panning up the camera
if (zoomDelta < 0 || transform.position.y < (zoomMax -20))
{
transform.eulerAngles = new Vector3(pan, 0, 0);
}
// Move camera with arrow keys
translation += new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
// Move camera with mouse
if (Input.GetMouseButton(2)) // MMB
{
// Hold button and drag camera around
translation -= new Vector3(Input.GetAxis("Mouse X") * dragSpeed * Time.deltaTime, 0,
Input.GetAxis("Mouse Y") * dragSpeed * Time.deltaTime);
}
else
{
// Move camera if mouse pointer reaches screen borders
if (Input.mousePosition.x < scrollArea)
{
translation += Vector3.right * -scrollSpeed * Time.deltaTime;
}
if (Input.mousePosition.x >= Screen.width - scrollArea)
{
translation += Vector3.right * scrollSpeed * Time.deltaTime;
}
if (Input.mousePosition.y < scrollArea)
{
translation += Vector3.forward * -scrollSpeed * Time.deltaTime;
}
if (Input.mousePosition.y > Screen.height - scrollArea)
{
translation += Vector3.forward * scrollSpeed * Time.deltaTime;
}
}
// Keep camera within level and zoom area
var desiredPosition = transform.position + translation;
if (desiredPosition.x < -levelArea || levelArea < desiredPosition.x)
{
translation.x = 0;
}
if (desiredPosition.y < zoomMin || zoomMax < desiredPosition.y)
{
translation.y = 0;
}
if (desiredPosition.z < -levelArea || levelArea < desiredPosition.z)
{
translation.z = 0;
}
// Move camera parallel to world axis
transform.position += translation;
}
}
I would like to have a smooth transition from the position where the camera is now and the desired position after scrolling in/out. And also I would like to know how to make the camera to stop at the minimum/maximum zoom distance rather than to stop close to it. Thank you for helping. Video how the camera movement looks like: https://youtu.be/Lt3atJEaOjA
Okay, so I'm going to do three things here. First and foremost, I'm going to recommend that if you're working with advanced camera behavior, you probably want to at least consider using Cinemachine. I'd walk you through it myself, but given my lack of personal experience with it, I'd probably be doing you a disservice by even trying. There are many good tutorials out there. Youtube and Google should provide.
The second thing I'll do is solve your problem in the most direct way I can manage, and after that, we'll see if we can't come up with a better method for solving your problem.
So, the key here is that Unity's scrollwheel input is pretty binary. When you check a scrollwheel axis, the result is directly based on how many "clicks" your wheel has gone through since the last frame update, but what you really want is something with a bit of give. By default, Unity can actually do this with most of its axis inputs: You might notice that if you use WASD in a default Unity project, there's a sort of "lag" to it, where you'll take your fingers off the keys but you'll still keep receiving positive values from Input.GetAxis() for a few frames. This is tied to the Gravity value in your input settings, and Input.GetAxisRaw() is actually used to circumvent this entirely. For whatever reason, scrollwheel axes don't seem to be affected by axis gravity, so we essentially have to implement something similar ourselves.
// Add this property to your class definition (so it persists between updates):
private float wheelAxis = 0;
// Delete this line:
var zoomDelta = Input.GetAxis("Mouse ScrollWheel") * zoomSpeed * Time.deltaTime;
// And put these three new lines in its place:
wheelAxis += Input.GetAxis("Mouse ScrollWheel");
wheelAxis = Mathf.MoveTowards(wheelTotal, 0f, Time.deltaTime);
var zoomDelta = Mathf.Clamp(wheelAxis, -0.05f, 0.05f) * zoomSpeed * Time.deltaTime;
Right, so we do a few things here. Every update, we add the current scrollwheel values to our wheelAxis. Next, we apply the current Time.deltatime as "gravity" via the Mathf.MoveTowards() function. Finally, we call what's mostly just your old zoomDelta code with a simple modification: We constrain the wheelAxis with Mathf.Clamp to try and regulate how fast the zooming can happen.
You can modify this code in a couple of ways. If you multiply the Time.deltaTime parameter you can affect how long your input will "persist" for. If you mess with the Mathf.Clamp() values, you'll get faster or slower zooming. All in all though, if you just want a smooth zoom with minimal changes to your code, that's probably your best bet.
So!
Now that we've done that, let's talk about your code, and how you're approaching the problem, and see if we can't find a somewhat cleaner solution.
Getting a good camera working is surprisingly non-trivial. Your code looks like a lot of code I see that tries to solve a complex problem: It looks like you added some feature, and then tested it, and found some edge cases where it fell apart, and then patched those cases, and then tried to implement a new feature on top of the old code, but it kinda broke in various other ways, etc etc, and what we've got at this point is a bit messy.
The biggest issue with your code is that the camera's position and the camera's rotation are closely tied together. When working with characters, this is usually fine, but when working with a camera, you want to break this up. Think about where the camera is and what the camera is looking at as very separate things to keep track of.
So here's a working camera script that you should be able to plug in and just run with:
using UnityEngine;
public class RTSCamera : MonoBehaviour
{
public float zoomSpeed = 100f;
public float zoomTime = 0.1f;
public float maxHeight = 100f;
public float minHeight = 20f;
public float focusHeight = 10f;
public float focusDistance = 20f;
public int panBorder = 25;
public float dragPanSpeed = 25f;
public float edgePanSpeed = 25f;
public float keyPanSpeed = 25f;
private float zoomVelocity = 0f;
private float targetHeight;
void Start()
{
// Start zoomed out
targetHeight = maxHeight;
}
void Update()
{
var newPosition = transform.position;
// First, calculate the height we want the camera to be at
targetHeight += Input.GetAxis("Mouse ScrollWheel") * zoomSpeed * -1f;
targetHeight = Mathf.Clamp(targetHeight, minHeight, maxHeight);
// Then, interpolate smoothly towards that height
newPosition.y = Mathf.SmoothDamp(transform.position.y, targetHeight, ref zoomVelocity, zoomTime);
// Always pan the camera using the keys
var pan = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")) * keyPanSpeed * Time.deltaTime;
// Optionally pan the camera by either dragging with middle mouse or when the cursor touches the screen border
if (Input.GetMouseButton(2)) {
pan -= new Vector2(Input.GetAxis("Mouse X"), Input.GetAxis("Mouse Y")) * dragPanSpeed * Time.deltaTime;
} else {
var border = Vector2.zero;
if (Input.mousePosition.x < panBorder) border.x -= 1f;
if (Input.mousePosition.x >= Screen.width - panBorder) border.x += 1f;
if (Input.mousePosition.y < panBorder) border.y -= 1f;
if (Input.mousePosition.y > Screen.height - panBorder) border.y += 1f;
pan += border * edgePanSpeed * Time.deltaTime;
}
newPosition.x += pan.x;
newPosition.z += pan.y;
var focusPosition = new Vector3(newPosition.x, focusHeight, newPosition.z + focusDistance);
transform.position = newPosition;
transform.LookAt(focusPosition);
}
}
While I encourage you to go through it in your own time, I'm not going to drag you through every inch of it. Instead, I'll just go over the main crux.
The key idea here is that rather than controlling the camera's height and orientation directly, we just let the scrollwheel dictate where want the camera's height to be, and then we use Mathf.SmoothDamp() to move the camera smoothly into that position over several frames. (Unity has many useful functions like this. Consider Mathf.MoveTowards() for an alternative interpolation method.) At the very end, rather than trying to fiddle with the camera's rotation values directly, we just pick a point in front of us near the ground and point the camera at that spot directly.
By keeping the camera's position and orientation completely independent of each other, as well as separating out the "animation" of the camera's height, we avoid a lot of headaches and eliminate a lot of potential for messy interwoven bugs.
I hope that helps.
Cinemachine is pretty good, but I suggest people learn on your own quite a bit before using Cinemachine so that you better understand what's going on in the background.
We're learning about steering behaviors in my artificial intelligence for games class, and I figured I'd try my hand at implementing some of them. I've mainly been reading The Nature of Code to familiarize myself with the topics.
Here is the repository for my Unity project. The relevant scene is under Assets/Scenes/Wandering.unity.
Here's the associated script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Wandering : MonoBehaviour
{
public float maxSpeed;
private float speed;
public float maxForce;
public float radius;
private Rigidbody body;
void Awake()
{
body = gameObject.GetComponent<Rigidbody>();
speed = maxSpeed;
body.velocity = new Vector3(5, 0, 5);
}
void Update()
{
// Get future position
Vector3 futurePosition = GetFuturePosition();
// Select random point on circle of radius "radius" around the future position
Vector3 target = GeneratePointOnCircle(futurePosition);
// Compute desired velocity as one pointing there
Vector3 desiredVelocity = GetDesiredVelocity(target);
// Get the steering force vector
Vector3 steerForce = desiredVelocity - body.velocity;
steerForce.y = 0;
// Cap the force that can be applied (lower max force = more difficult to turn)
if (Vector3.Magnitude(steerForce) > maxForce)
{
steerForce = Vector3.Normalize(steerForce) * maxForce;
}
// Apply the force to the body
body.AddForce(steerForce);
}
/* Returns a random point on a circle positioned at the given center and radius.
*/
Vector3 GeneratePointOnCircle(Vector3 center)
{
Vector3 point = center;
float angle = Random.Range(0, 360) * Mathf.Deg2Rad;
point.x += radius * Mathf.Cos(angle);
point.z += radius * Mathf.Sin(angle);
return point;
}
/* Computes and returns the future, predicted position of this object, assuming
* it continues traveling in its current direction at its current speed.
*/
Vector3 GetFuturePosition()
{
// We have a current velocity
// We have a time elapsed
// We have a current position
// Future position = current position + current velocity * delta time
return transform.position + body.velocity * Time.deltaTime;
}
/* The desired velocity is simply the unit vector in the direction of the target
* scaled by the speed of the object.
*/
Vector3 GetDesiredVelocity(Vector3 target)
{
return Vector3.Normalize(target - transform.position) * speed;
}
}
Values set in the editor:
maxSpeed: 40
maxForce: 20
radius: 60
When I run this, the agent does not behave as intended. The main problem is that instead of traveling smoothly, it stutters around in brief bursts, seems to pause, and then starts moving again. This random behavior is still pretty neat, and sort of mimics that of a disoriented rodent, but I'm looking for more intelligent-seeming behavior.
Is there a flaw in my script or logic that's forcing the agent to behave in this erratic manner? Any advice would be greatly appreciated.
It seems the problem was my use of Time.deltaTime for calculating the predicted point in the future if the agent were to continue at its current velocity.
Since this is really the time elapsed since the last frame update, it's quite a small number. Thus, using it for predicting the future point was misleading and would produce points very close to the agent (hence the "stuttering" behavior).
Instead, I opted to use a fixed "lookahead" time (say 2) to predict further into the future.