I am trying to emulate acceleration and deceleration in Unity.
I have written to code to generate a track in Unity and place an object at a specific location on the track based on time. The result looks a little like this.
The issue I currently have is that each section of the spline is a different length and the cube moves across each section at a different, but uniform, speed. This causes there to be sudden jumps in the change of the speed of the cube when transitioning between sections.
In order to try and fix this issue, I attempted to use Robert Penner's easing equations on the GetTime(Vector3 p0, Vector3 p1, float alpha) method. However, whilst this did help somewhat, it was not sufficient. There were still jumps in speed in between transitions.
Does anyone have any ideas on how I could dynamically ease the position of the cube to make it look like it was accelerating and decelerating, without large jumps in speed between segments of the track?
I have written a script that shows a simple implementation of my code. It can be attached to any game object. To make it easy to see what is happening when the code runs, attach to something like a cube or sphere.
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class InterpolationExample : MonoBehaviour {
[Header("Time")]
[SerializeField]
private float currentTime;
private float lastTime = 0;
[SerializeField]
private float timeModifier = 1;
[SerializeField]
private bool running = true;
private bool runningBuffer = true;
[Header("Track Settings")]
[SerializeField]
[Range(0, 1)]
private float catmullRomAlpha = 0.5f;
[SerializeField]
private List<SimpleWayPoint> wayPoints = new List<SimpleWayPoint>
{
new SimpleWayPoint() {pos = new Vector3(-4.07f, 0, 6.5f), time = 0},
new SimpleWayPoint() {pos = new Vector3(-2.13f, 3.18f, 6.39f), time = 1},
new SimpleWayPoint() {pos = new Vector3(-1.14f, 0, 4.55f), time = 6},
new SimpleWayPoint() {pos = new Vector3(0.07f, -1.45f, 6.5f), time = 7},
new SimpleWayPoint() {pos = new Vector3(1.55f, 0, 3.86f), time = 7.2f},
new SimpleWayPoint() {pos = new Vector3(4.94f, 2.03f, 6.5f), time = 10}
};
[Header("Debug")]
[Header("WayPoints")]
[SerializeField]
private bool debugWayPoints = true;
[SerializeField]
private WayPointDebugType debugWayPointType = WayPointDebugType.SOLID;
[SerializeField]
private float debugWayPointSize = 0.2f;
[SerializeField]
private Color debugWayPointColour = Color.green;
[Header("Track")]
[SerializeField]
private bool debugTrack = true;
[SerializeField]
[Range(0, 1)]
private float debugTrackResolution = 0.04f;
[SerializeField]
private Color debugTrackColour = Color.red;
[System.Serializable]
private class SimpleWayPoint
{
public Vector3 pos;
public float time;
}
[System.Serializable]
private enum WayPointDebugType
{
SOLID,
WIRE
}
private void Start()
{
wayPoints.Sort((x, y) => x.time.CompareTo(y.time));
wayPoints.Insert(0, wayPoints[0]);
wayPoints.Add(wayPoints[wayPoints.Count - 1]);
}
private void LateUpdate()
{
//This means that if currentTime is paused, then resumed, there is not a big jump in time
if(runningBuffer != running)
{
runningBuffer = running;
lastTime = Time.time;
}
if(running)
{
currentTime += (Time.time - lastTime) * timeModifier;
lastTime = Time.time;
if(currentTime > wayPoints[wayPoints.Count - 1].time)
{
currentTime = 0;
}
}
transform.position = GetPosition(currentTime);
}
#region Catmull-Rom Math
public Vector3 GetPosition(float time)
{
//Check if before first waypoint
if(time <= wayPoints[0].time)
{
return wayPoints[0].pos;
}
//Check if after last waypoint
else if(time >= wayPoints[wayPoints.Count - 1].time)
{
return wayPoints[wayPoints.Count - 1].pos;
}
//Check time boundaries - Find the nearest WayPoint your object has passed
float minTime = -1;
float maxTime = -1;
int minIndex = -1;
for(int i = 1; i < wayPoints.Count; i++)
{
if(time > wayPoints[i - 1].time && time <= wayPoints[i].time)
{
maxTime = wayPoints[i].time;
int index = i - 1;
minTime = wayPoints[index].time;
minIndex = index;
}
}
float timeDiff = maxTime - minTime;
float percentageThroughSegment = 1 - ((maxTime - time) / timeDiff);
//Define the 4 points required to make a Catmull-Rom spline
Vector3 p0 = wayPoints[ClampListPos(minIndex - 1)].pos;
Vector3 p1 = wayPoints[minIndex].pos;
Vector3 p2 = wayPoints[ClampListPos(minIndex + 1)].pos;
Vector3 p3 = wayPoints[ClampListPos(minIndex + 2)].pos;
return GetCatmullRomPosition(percentageThroughSegment, p0, p1, p2, p3, catmullRomAlpha);
}
//Prevent Index Out of Array Bounds
private int ClampListPos(int pos)
{
if(pos < 0)
{
pos = wayPoints.Count - 1;
}
if(pos > wayPoints.Count)
{
pos = 1;
}
else if(pos > wayPoints.Count - 1)
{
pos = 0;
}
return pos;
}
//Math behind the Catmull-Rom curve. See here for a good explanation of how it works. https://stackoverflow.com/a/23980479/4601149
private Vector3 GetCatmullRomPosition(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float alpha)
{
float dt0 = GetTime(p0, p1, alpha);
float dt1 = GetTime(p1, p2, alpha);
float dt2 = GetTime(p2, p3, alpha);
Vector3 t1 = ((p1 - p0) / dt0) - ((p2 - p0) / (dt0 + dt1)) + ((p2 - p1) / dt1);
Vector3 t2 = ((p2 - p1) / dt1) - ((p3 - p1) / (dt1 + dt2)) + ((p3 - p2) / dt2);
t1 *= dt1;
t2 *= dt1;
Vector3 c0 = p1;
Vector3 c1 = t1;
Vector3 c2 = (3 * p2) - (3 * p1) - (2 * t1) - t2;
Vector3 c3 = (2 * p1) - (2 * p2) + t1 + t2;
Vector3 pos = CalculatePosition(t, c0, c1, c2, c3);
return pos;
}
private float GetTime(Vector3 p0, Vector3 p1, float alpha)
{
if(p0 == p1)
return 1;
return Mathf.Pow((p1 - p0).sqrMagnitude, 0.5f * alpha);
}
private Vector3 CalculatePosition(float t, Vector3 c0, Vector3 c1, Vector3 c2, Vector3 c3)
{
float t2 = t * t;
float t3 = t2 * t;
return c0 + c1 * t + c2 * t2 + c3 * t3;
}
//Utility method for drawing the track
private void DisplayCatmullRomSpline(int pos, float resolution)
{
Vector3 p0 = wayPoints[ClampListPos(pos - 1)].pos;
Vector3 p1 = wayPoints[pos].pos;
Vector3 p2 = wayPoints[ClampListPos(pos + 1)].pos;
Vector3 p3 = wayPoints[ClampListPos(pos + 2)].pos;
Vector3 lastPos = p1;
int maxLoopCount = Mathf.FloorToInt(1f / resolution);
for(int i = 1; i <= maxLoopCount; i++)
{
float t = i * resolution;
Vector3 newPos = GetCatmullRomPosition(t, p0, p1, p2, p3, catmullRomAlpha);
Gizmos.DrawLine(lastPos, newPos);
lastPos = newPos;
}
}
#endregion
private void OnDrawGizmos()
{
#if UNITY_EDITOR
if(EditorApplication.isPlaying)
{
if(debugWayPoints)
{
Gizmos.color = debugWayPointColour;
foreach(SimpleWayPoint s in wayPoints)
{
if(debugWayPointType == WayPointDebugType.SOLID)
{
Gizmos.DrawSphere(s.pos, debugWayPointSize);
}
else if(debugWayPointType == WayPointDebugType.WIRE)
{
Gizmos.DrawWireSphere(s.pos, debugWayPointSize);
}
}
}
if(debugTrack)
{
Gizmos.color = debugTrackColour;
if(wayPoints.Count >= 2)
{
for(int i = 0; i < wayPoints.Count; i++)
{
if(i == 0 || i == wayPoints.Count - 2 || i == wayPoints.Count - 1)
{
continue;
}
DisplayCatmullRomSpline(i, debugTrackResolution);
}
}
}
}
#endif
}
}
Ok, let's put some math on this.
I've always been and advocate of the importance and utility of math in gamedev, and maybe I go too far into this on this answer, but I really think your question is not about coding at all, but about modelling and solving an algebra problem. Anyway, let´s go.
Parametrization
If you have a college degree, you may remember something about functions - operations that take a parameter and yield a result - and graphs - a graphic representation (or plot) of the evolution of a function vs. its parameter. f(x) may remind you something: it says that a function named f depends on the prameter x. So, "to parameterize" roughly means expressing a system it in terms of one or more parameters.
You may not be familiarized with the terms, but you do it all the time. Your Track, for example, is a system with 3 parameters: f(x,y,z).
One interesting thing about parameterization is that you can grab a system and describe it in terms of other parameters. Again, you are already doing it. When you describe the evolution of your track with time, you are sayng that each coordinate is a function of time, f(x,y,z) = f(x(t),y(t),z(t)) = f(t). In other words, you can use time to calculate each coordinate, and use the coordinates to position your object in space for that given time.
Modelling a Track System
Finally, I'll start answering your question. For describing completely the Track system you want, you will need two things:
A path;
You practically solved this part already. You set up some points in Scene space and use a Catmull–Rom spline to interpolate the points and generate a path. That is clever, and there is no much left to do about it.
Also, you added a field time on each point so you want to asure the moving object will pass through this check at this exact time. I'll be back on this later.
A moving object.
One interesting thing about your Path solution is that you parameterized the path calculation with a percentageThroughSegment parameter - a value ranging from 0 to 1 representing the relative position inside the segment. In your code, you iterate at fixed time steps, and your percentageThroughSegment will be the proportion between the time spent and the total time span of the segment. As each segment have a specific time span, you emulate many constant speeds.
That's pretty standard, but there is one subtlety. You are ignoring a hugely important part on describing a movement: the distance traveled.
I suggest you a different approach. Use the distance traveled to parameterize your path. Then, the object's movement will be the distance traveled parameterized with respect to time. This way you will have two independent and consistent systems. Hands to work!
Example:
From now on, I'll make everything 2D for the sake of simplicity, but changing it to 3D later will be trivial.
Consider the following path:
Where i is the index of the segment, d is the distance traveled and x, y are the coords in plane. This could be a path created by a spline like yours, or with Bézier curves or whatever.
The movement developed by a object with your current solution could be described as a graph of distance traveled on the path vs time like this:
Where t in the table is the time that the object must reach the check, d is again the distance traveled up to this position, v is the velocity and a is the acceleration.
The upper shows how the object advances with time. The horizontal axis is the time and the vertical is the distance traveled. We can imagine that the vertical axis is the path "unrolled" in a flat line. The lower graph is the evolution of the speed over time.
We must recall some physics at this point and note that, at each segment, the graph of the distance is a straight line, that corresponds to a movement at constant speed, with no acceleration. Such a system is described by this equation: d = do + v*t
Whenever the object reaches the check points, its speed value suddenly changes (as there is no continuity in its graph) and that has a weird effect in the scene. Yes, you already know that and that's precisely why you posted the question.
Ok, how can we make that better? Hmm... if the speed graph were continuous, the wouldn't be that annoying speed jump. The simplest description of a movement like this could be an uniformly acelerated. Such a system is described by this equation: d = do + vo*t + a*t^2/2. We will also have to assume an initial velocity, I'll choose zero here (parting from rest).
Like we expected, The velocity graph is continuous, the movement is accelerated throug the path. This could be coded into Unity changing the methids Start and GetPosition like this:
private List<float> lengths = new List<float>();
private List<float> speeds = new List<float>();
private List<float> accels = new List<float>();
public float spdInit = 0;
private void Start()
{
wayPoints.Sort((x, y) => x.time.CompareTo(y.time));
wayPoints.Insert(0, wayPoints[0]);
wayPoints.Add(wayPoints[wayPoints.Count - 1]);
for (int seg = 1; seg < wayPoints.Count - 2; seg++)
{
Vector3 p0 = wayPoints[seg - 1].pos;
Vector3 p1 = wayPoints[seg].pos;
Vector3 p2 = wayPoints[seg + 1].pos;
Vector3 p3 = wayPoints[seg + 2].pos;
float len = 0.0f;
Vector3 prevPos = GetCatmullRomPosition(0.0f, p0, p1, p2, p3, catmullRomAlpha);
for (int i = 1; i <= Mathf.FloorToInt(1f / debugTrackResolution); i++)
{
Vector3 pos = GetCatmullRomPosition(i * debugTrackResolution, p0, p1, p2, p3, catmullRomAlpha);
len += Vector3.Distance(pos, prevPos);
prevPos = pos;
}
float spd0 = seg == 1 ? spdInit : speeds[seg - 2];
float lapse = wayPoints[seg + 1].time - wayPoints[seg].time;
float acc = (len - spd0 * lapse) * 2 / lapse / lapse;
float speed = spd0 + acc * lapse;
lengths.Add(len);
speeds.Add(speed);
accels.Add(acc);
}
}
public Vector3 GetPosition(float time)
{
//Check if before first waypoint
if (time <= wayPoints[0].time)
{
return wayPoints[0].pos;
}
//Check if after last waypoint
else if (time >= wayPoints[wayPoints.Count - 1].time)
{
return wayPoints[wayPoints.Count - 1].pos;
}
//Check time boundaries - Find the nearest WayPoint your object has passed
float minTime = -1;
// float maxTime = -1;
int minIndex = -1;
for (int i = 1; i < wayPoints.Count; i++)
{
if (time > wayPoints[i - 1].time && time <= wayPoints[i].time)
{
// maxTime = wayPoints[i].time;
int index = i - 1;
minTime = wayPoints[index].time;
minIndex = index;
}
}
float spd0 = minIndex == 1 ? spdInit : speeds[minIndex - 2];
float len = lengths[minIndex - 1];
float acc = accels[minIndex - 1];
float t = time - minTime;
float posThroughSegment = spd0 * t + acc * t * t / 2;
float percentageThroughSegment = posThroughSegment / len;
//Define the 4 points required to make a Catmull-Rom spline
Vector3 p0 = wayPoints[ClampListPos(minIndex - 1)].pos;
Vector3 p1 = wayPoints[minIndex].pos;
Vector3 p2 = wayPoints[ClampListPos(minIndex + 1)].pos;
Vector3 p3 = wayPoints[ClampListPos(minIndex + 2)].pos;
return GetCatmullRomPosition(percentageThroughSegment, p0, p1, p2, p3, catmullRomAlpha);
}
Ok, let's see how it goes...
Err... uh-oh.
It looked almost good, except that at some point it move backwards and then advance again. Actually, if we check our graphs, it is described there. Between 12 and 16 sec the velocity is negatie. Why does this happen? Because this function of movement (constant accelerations), altough simple, have some limitations. With some abrupt velocity variations, there may not be a constant value of acceleration that can guarantee our premise (passing on checkpoints at correct time) without have side-effects like those.
What do we do now?
You have plenty of options:
Describe a system with linear acceleration changes and apply boundary conditions (Warning: lots of equations to solve);
Describe a system with constant acceleraction for some time period, like accelerate or decelerate just before/after curve, then keep constant speed for the rest of the segment (Warning: even more equations to solve, hard to guarantee the premise of passing checkpoints in correct time);
Use an interpolation method to generate a graph of position through time. I've tried Catmull-Rom itself, but I didn't like the result, because the speed didn't look very smooth. Bezier Curves seem to be a preferable approach because you can manipulate the slopes (aka speeds) on the control points directally and avoid backward movements;
And my favourite: add a public AnimationCurve field on the class and customize your movement graph in Editor with ts awesome built-in drawer! You can easily add control points with its AddKey method and fetch position for a time with its Evaluate method.
You could even use OnValidate method on your component class to automatically update the points in the Scene when you edit it in the curve and vice-versa.
Don't stop there! Add a gradient on the path's line Gizmo to easily see where it goes faster or slower, add handles for manipulating path in editor mode... get creative!
As far as I can tell you already have most of the solution in, just initialized improperly.
The local speed is dependent on the length of the spline, so you should modulate the speed by the inverse of the length of the segment (which you can easily approximate with a few steps).
Granted, in your case you don't have control over the speed, only the input time, so what you need is to distribute properly the values of SimpleWayPoint.time according to the order and length of previous spline segments, instead of initializing it manually in the field declaration. This way percentageThroughSegment should be evenly distributed.
As mentioned in comments, some of that math could look simpler with Lerp() :)
Let's define some terms first:
t: interpolation variable for each spline, ranging from 0 to 1.
s: the length of each spline. Depending on what type of spline you use (catmull-rom, bezier, etc.), there are formulas to calculate the estimated total length.
dt: the change in t per frame. In your case, if this is constant across all the different splines, you will see sudden speed change at spline end points, as each spline has a different length s.
The simplest way to ease the speed change at each joint is:
void Update() {
float dt = 0.05f; //this is currently your "global" interpolation speed, for all splines
float v0 = s0/dt; //estimated linear speed in the first spline.
float v1 = s1/dt; //estimated linear speed in the second spline.
float dt0 = interpSpeed(t0, v0, v1) / s0; //t0 is the current interpolation variable where the object is at, in the first spline
transform.position = GetCatmullRomPosition(t0 + dt0*Time.deltaTime, ...); //update your new position in first spline
}
where:
float interpSpeed(float t, float v0, float v1, float tEaseStart=0.5f) {
float u = (t - tEaseStart)/(1f - tEaseStart);
return Mathf.Lerp(v0, v1, u);
}
The intuition above is that as I am reaching the end of my first spline, I predict the expected speed in the next spline, and ease my current speed to reach there.
Finally, in order to make the easing look even better:
Consider using a non-linear interpolation function in interpSpeed().
Consider implementing an "ease-into" also at the start of the second spline
You could try work with the wheelcollider tutorial they have for their wheel system.
It has some variables you can adjust along with the Rigidbody variables to achieve simulated driving.
As they write
You can have up to 20 wheels on a single vehicle instance, with each of them applying steering, motor or braking torque.
Disclaimer: I have only minimal experience working with WheelColliders. But they seem like what you're looking for to me.
https://docs.unity3d.com/Manual/WheelColliderTutorial.html
Related
I am Programming a random "Stone" Spawner and have a big problem at the moment. I have some ideas how to fix it, but want to know a performance friendly way to do it.
So my way to Spawn the Objects on the Sphere Surface is this one:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SpawnObjects : MonoBehaviour
{
public Vector3 centerOfSphere = new Vector3(0, 5000, 0);
public float radiusOfSphere = 5000.0f;
public List<GameObject> stones;
public int stonesToSpawn = 1;
void Start()
{
Vector3 center = transform.position + centerOfSphere; //Setting the center position of the Sphere
//This for loop is spawning the Stones on a random position on Sphere Surface
for (int i = 0; i < stonesToSpawn; i++)
{
Vector3 pos = RandomCircle(center, radiusOfSphere);
Quaternion rot = Quaternion.FromToRotation(Vector3.forward, center - pos);
Instantiate(stones[Random.Range(0, stones.Count)], pos, rot);
}
}
//Method returns a Random Position on a Sphere
Vector3 RandomCircle(Vector3 center, float radius)
{
float alpha = UnityEngine.Random.value * 360;
float beta = UnityEngine.Random.value * 360;
Vector3 pos;
pos.x = radius * Mathf.Cos(beta) * Mathf.Cos(alpha);
pos.y = radius * Mathf.Cos(beta) * Mathf.Sin(alpha);
pos.z = radius * Mathf.Sin(beta);
return pos;
}
}
So thank you for your following explanations! :)
As said to make your live one step easier simply use Random.onUnitSphere so your entire method RandomCircle (change that name btw!) can be shrinked to
private Vector3 RandomOnSphere(Vector3 center, float radius)
{
return center + Random.onUnitSphere * radius;
}
And then in order to have a minimum distance between them there are probably multiple ways but I guess the simplest - brute force - way would be:
store already used positions
when you get a new random position check the distance to already existing ones
keep get new random positions until you have found one, that is not too close to an already existing one
This depends of course a lot on your use case and the amount of objects and the minimum distance etc - in other words I leave it up to you to assure that the requested amount and minimum distance is doable at all with the given sphere radius.
You could always leave an "emergency exit" and give up after e.g. 100 attempts.
Something like e.g.
// Linq offers some handy query shorthands that allow to shorten
// long foreach loops into single calls
using System.Linq;
...
private const int MAX_ATTEMPTS = 100;
public float minimumDistance = 1f;
void Start()
{
var center = transform.position + centerOfSphere;
// It is cheaper to use with square magnitudes
var minDistanceSqr = minimumDistance * minimumDistance;
// For storing the already used positions
// Already initialize with the correct capacity, this saves resources
var usedPositions = new List<Vector3>(stonesToSpawn);
for (int i = 0; i < stonesToSpawn; i++)
{
// Keep track of the attempts (for the emergency break)
var attempts = 0;
Vector3 pos = Vector3.zero;
do
{
// Get a new random position
pos = RandomOnSphere(center, radiusOfSphere);
// increase the attempts
attempts++;
// We couldn't find a "free" position within the 100 attempts :(
if(attempts >= MAX_ATTEMPTS)
{
throw new Exception ("Unable to find a free spot! :'(");
}
}
// As the name suggests this checks if any "p" in "usedPositions" is too close to the given "pos"
while(usedPositions.Any(p => (p - pos).sqrMagnitude <= minDistanceSqr)));
var rot = Quaternion.FromToRotation(Vector3.forward, center - pos);
Instantiate(stones[Random.Range(0, stones.Count)], pos, rot);
// Finally add this position to the used ones so the next iteration
// also checks against this position
usedPositions.Add(pos);
}
}
Where
usedPositions.Any(p => (p - pos).sqrMagnitude <= minDistanceSqr))
basically equals doing something like
private bool AnyPointTooClose(Vector3 pos, List<Vector3> usedPositions, float minDistanceSqr)
{
foreach(var p in usedPositions)
{
if((p - pos).sqrMagnitude <= minDistanceSqr)
{
return true;
}
}
return false;
}
if that's better to understand for you
This is more of a math question than a coding question. I would like to reach for example an rotation angle of 90 in 1 second while speed is accelerating at constant value. My current version takes 1.4 seconds to reach the desired rotation angle, and it should reach it in 1 second. I believe that the reason for that is that it currently accelerates to speed of 90 in 1 second and not to rotation angle of 90. Since I am not that good in math, I have no idea how I need to adjust the acceleration calculation. I am unable to find any solution to this.
NOTE: I need to adjust the rotation angles manually, I am not able to use any existing functions, like for example transform.Rotate(), since in my complete version the rotation direction can change at any time and the rotation also has deceleration value.
This is a very simplified version of what I have (it only rotates the z axis to one direction and runs once on start):
private float accelerationInSeconds = 1;
private float targetAngle = 90f;
private float speed = 0;
private float axis = 1;
private bool rotate = true;
private float acceleration;
void Start() {
// Calculate acceleration (this calculation should be changed)
acceleration = targetAngle / accelerationInSeconds;
}
void Update() {
if (rotate) {
// Accelerate
speed += axis * (acceleration * Time.deltaTime);
// Calculate next rotation position
Vector3 rotationVector = transform.rotation.eulerAngles;
rotationVector.z += speed * Time.deltaTime;
// Rotate object
transform.rotation = Quaternion.Euler(rotationVector);
// Check if rotation has gone over the target angle
if (rotationVector.z >= targetAngle) {
rotationVector.z = targetAngle;
speed = 0;
rotate = false;
}
}
}
Thanks in advance for anyone who can help!
EDIT: Modified code to be more efficient. I can't use RotateTowards() since in my complete code I need to clamp the rotation between targetAngle and negative targetAngle. Hopefully this code is more efficient and performance friendly. But I still have not found a solution for my original math related question, which was the whole point of this question.
private float accelerationInSeconds = 1;
private float targetAngle = 90f;
private float speed = 0;
private float angle = 0;
private float axis = 1;
private bool rotate = true;
private float acceleration;
void Start() {
// Calculate acceleration (this calculation should be changed)
acceleration = targetAngle / accelerationInSeconds;
}
void Update() {
if (rotate) {
// Accelerate
speed += axis * (acceleration * Time.deltaTime);
// Calculate next rotation position
angle += speed * Time.deltaTime;
// Check if rotation has gone over the target angle
if (angle >= targetAngle) {
angle = targetAngle;
speed = 0;
rotate = false;
}
// Rotate object
transform.rotation = Quaternion.AngleAxis(angle, Vector3.forward);
}
}
I finally figured it out, thanks to Math section in StackExchange.
So the simple answer is this:
acceleration = 2 * targetAngle / Mathf.Pow(accelerationInSeconds, 2);
As was suggested before I would use a Coroutine. Coroutines are like temporary Update methods and often easier to control and maintain than doing stuff directly in Update.
// Flag to avoid concurrent routines
private bool isRotating;
public void Rotate(float targetAngle, float duration)
{
if(! isRotating) StartCoroutine (RotateRoutine(targetAngle, duration));
}
private IEnumerator RotateRoutine (float targetAngle, float duration)
{
// Just to be sure
if(isRotating) yield break;
// block concurrent routines
isRotating = true;
// Pre-calculate the start and end rotation
var start = transform.rotation;
var end = Quaternion.Euler(0, 0, targetAngle);
var timePassed = 0f;
while(timePassed < duration)
{
// This value will grow linear from 0 to 1 in exactly "duration" seconds
var x = timePassed / duration;
// TODO!
var y = MAGIC;
// Interpolate between the start and end rotation using given factor "y"
transform.rotation = Quaternion.Lerp(start, end, y);
// "pause" the routine here, render this frame
// and continue from here in the next frame
yield return null;
// Increase by the time passed since last frame
timePassed += Time.deltaTime;
}
// To be sure to end with clean values
transform.rotation = end;
// Allow next routine
isRotating = false;
}
So what do we have to fill in for MAGIC?
Basically it can be any mathematical function that maps given input 0 to 1 to 0 to 1.
There are multiple possibilities.
What you currently ask for is a linear growing speed. That means the resulting movement shall be quadratic! So we already know the Formular
var y = a * x * x + b;
We further know from your code that speed always starts from 0 -> b = 0. And the last step is pretty straight forward:
What value do we have to fill in so y goes from 0 to 1 at the same time that x goes from 0 to 1?
1 = a * 1 * 1 + 0;
=> a = 1!
So in your case it is simply
var y = x * x;
If you also want ease-out you could also simply use Mathf.Smoothstep which automatically adds ease-in and ease-out
var y = Mathf.SmoothStep(0, 1, x);
To make it even easier to control you could use an AnimationCurve and adjust the movement curve exactly to your needs in the Inspector
[SerializeField] private AnimationCurve curve;
The curve editor already comes with some preset curves like e.g. linear, logarithmic, exponential and eased-in/-out grow from 0 to 1!
And then use AnimationCurve.Evaluate to get the value (y) in the routine for a given input time (x).
var y = curve.Evaluate(x);
As the question explains, how to find a point(vector2) between two points(Vector2) of a line (line renderer),
I am using a line renderer to draw the line with two points (start with vector and endpoint), what I want is that want point A in the line as you can see in the image.
In theory, I want to shorten the line from the original length for my use case, I am doing a line intersection check for two lines overlapping, but the problem is that in some cases the lines start from the same point, and in those situations, if the points meet, it still counts as a line intersection. So in theory I want to shorten the line for line intersection check so it doesn't intersect with the other line if the points meet.
Here is the script for my line intersection check!
public bool isIntersecting = false;
LineRenderer lineRenderer;
private void Start()
{
lineRenderer = this.GetComponent<LineRenderer>();
}
private void Update()
{
LineRenderer recievedLine;
if (FindObjectOfType<LineDynamics>().ropeRenderer != null)
{
recievedLine = FindObjectOfType<LineDynamics>().ropeRenderer;
}
else
{
return;
}
AreLineSegmentsIntersectingDotProduct(lineRenderer.GetPosition(0), lineRenderer.GetPosition(lineRenderer.positionCount - 1), recievedLine.GetPosition(0), recievedLine.GetPosition(recievedLine.positionCount - 1));
}
//Line segment-line segment intersection in 2d space by using the dot product
//p1 and p2 belongs to line 1, and p3 and p4 belongs to line 2
public bool AreLineSegmentsIntersectingDotProduct(Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4)
{
if (IsPointsOnDifferentSides(p1, p2, p3, p4) && IsPointsOnDifferentSides(p3, p4, p1, p2))
{
isIntersecting = true;
}
else
{
isIntersecting = false;
}
return isIntersecting;
}
//Are the points on different sides of a line?
private bool IsPointsOnDifferentSides(Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4)
{
bool isOnDifferentSides = false;
//The direction of the line
Vector2 lineDir = p2 - p1;
//The normal to a line is just flipping x and z and making z negative
Vector2 lineNormal = new Vector2(-lineDir.y, lineDir.x);
//Now we need to take the dot product between the normal and the points on the other line
float dot1 = Vector2.Dot(lineNormal, p3 - p1);
float dot2 = Vector2.Dot(lineNormal, p4 - p1);
//If you multiply them and get a negative value then p3 and p4 are on different sides of the line
if (dot1 * dot2 < 0f)
{
isOnDifferentSides = true;
}
return isOnDifferentSides;
}
Any suggestion and help will be appreciated.
To your question
You could e.g. use Vector2.Lerp in order to get any point between your start and end point given a factor between 0 and 1 where
0: equal to start point
1: equal to end point
{0;1}: equal to linear interpolation between start and end
For the center it would e.g. be
var center = Vector2.Lerp(startPosition, endPosition, 0.5f);
If you rather need a certain travel distance from the start point you could use Vector2.MoveTowards like
var otherPoint = Vector2.MoveTowards(startPoint, endPoint, desiredDistanceFromStartPoint);
Basically you also can go fully manual and basically do the same like
var direction = (endPoint - startPoint).normalized;
var otherPoint = startPoint + direction * desiredDistanceFromStartPoint;
This however, doesn't protect you from overshooting the endPoint.
Ton your actual problem
You know that there is already functions implemented by the Unity community long ago under Wiki - Math functions which already provides e.g.
//Calculate the intersection point of two lines. Returns true if lines intersect, otherwise false.
//Note that in 3d, two lines do not intersect most of the time. So if the two lines are not in the
//same plane, use ClosestPointsOnTwoLines() instead.
public static bool LineLineIntersection(out Vector3 intersection, Vector3 linePoint1, Vector3 lineVec1, Vector3 linePoint2, Vector3 lineVec2){
Vector3 lineVec3 = linePoint2 - linePoint1;
Vector3 crossVec1and2 = Vector3.Cross(lineVec1, lineVec2);
Vector3 crossVec3and2 = Vector3.Cross(lineVec3, lineVec2);
float planarFactor = Vector3.Dot(lineVec3, crossVec1and2);
//is coplanar, and not parrallel
if(Mathf.Abs(planarFactor) < 0.0001f && crossVec1and2.sqrMagnitude > 0.0001f)
{
float s = Vector3.Dot(crossVec3and2, crossVec1and2) / crossVec1and2.sqrMagnitude;
intersection = linePoint1 + (lineVec1 * s);
return true;
}
else
{
intersection = Vector3.zero;
return false;
}
}
//Two non-parallel lines which may or may not touch each other have a point on each line which are closest
//to each other. This function finds those two points. If the lines are not parallel, the function
//outputs true, otherwise false.
public static bool ClosestPointsOnTwoLines(out Vector3 closestPointLine1, out Vector3 closestPointLine2, Vector3 linePoint1, Vector3 lineVec1, Vector3 linePoint2, Vector3 lineVec2){
closestPointLine1 = Vector3.zero;
closestPointLine2 = Vector3.zero;
float a = Vector3.Dot(lineVec1, lineVec1);
float b = Vector3.Dot(lineVec1, lineVec2);
float e = Vector3.Dot(lineVec2, lineVec2);
float d = a*e - b*b;
//lines are not parallel
if(d != 0.0f){
Vector3 r = linePoint1 - linePoint2;
float c = Vector3.Dot(lineVec1, r);
float f = Vector3.Dot(lineVec2, r);
float s = (b*f - c*e) / d;
float t = (a*f - c*b) / d;
closestPointLine1 = linePoint1 + lineVec1 * s;
closestPointLine2 = linePoint2 + lineVec2 * t;
return true;
}
else{
return false;
}
}
And as said if your concern is actually the start point being equal for two lines (very unlikely) doing 50 Vector3 == checks is very cheap compared with the rest of calculations you are doing anyway.
using (unity 2019.3.7f1) 2d.
I have a player that moves around using a pullback mechanic and has a max power(like in Angry Birds).
I'm trying to draw a line(using a line renderer) that shows the exact path the player will go. I'm trying to make the line curve just like the player's path will. so far I've only managed to make a straight line in a pretty scuffed way.
The known variables are the Jump Power and the player's position, there is no friction. and I believe gravity is a constant(-9.81). Also, I would like to have a variable that allows me to control the line's length. And, if possible, the line will not go through objects and would act as if it has a collider.
// Edit
This is my current code. I changed The function so it would return the list's points because I wanted to be able to access it in Update() so it would only draw while I hold my mouse button.
My problem is that the trajectory line doesn't seem to curve, it goes in the right angle but it's straight. the line draws in the right direction and angle, but my initial issue of the line not curving remains unchanged. If you could please come back to me with an answer I would appreciate it.
enter code here
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TrajectoryShower : MonoBehaviour
{
LineRenderer lr;
public int Points;
public GameObject Player;
private float collisionCheckRadius = 0.1f;
public float TimeOfSimulation;
private void Awake()
{
lr = GetComponent<LineRenderer>();
lr.startColor = Color.white;
}
// Update is called once per frame
void Update()
{
if (Input.GetButton("Fire1"))
{
lr.positionCount = SimulateArc().Count;
for (int a = 0; a < lr.positionCount;a++)
{
lr.SetPosition(a, SimulateArc()[a]);
}
}
if (Input.GetButtonUp("Fire1"))
{
lr.positionCount = 0;
}
}
private List<Vector2> SimulateArc()
{
float simulateForDuration = TimeOfSimulation;
float simulationStep = 0.1f;//Will add a point every 0.1 secs.
int steps = (int)(simulateForDuration / simulationStep);
List<Vector2> lineRendererPoints = new List<Vector2>();
Vector2 calculatedPosition;
Vector2 directionVector = Player.GetComponent<DragAndShoot>().Direction;// The direction it should go
Vector2 launchPosition = transform.position;//Position where you launch from
float launchSpeed = 5f;//The initial power applied on the player
for (int i = 0; i < steps; ++i)
{
calculatedPosition = launchPosition + (directionVector * ( launchSpeed * i * simulationStep));
//Calculate gravity
calculatedPosition.y += Physics2D.gravity.y * (i * simulationStep);
lineRendererPoints.Add(calculatedPosition);
if (CheckForCollision(calculatedPosition))//if you hit something
{
break;//stop adding positions
}
}
return lineRendererPoints;
}
private bool CheckForCollision(Vector2 position)
{
Collider2D[] hits = Physics2D.OverlapCircleAll(position, collisionCheckRadius);
if (hits.Length > 0)
{
for (int x = 0;x < hits.Length;x++)
{
if (hits[x].tag != "Player" && hits[x].tag != "Floor")
{
return true;
}
}
}
return false;
}
}
Here's a simple way to visualize this.
To create your line you want a bunch of points.
The points represents the player's positions after being fired after X amount of time.
The position of each point is going to be : DirectionVector * (launch speed * time elapse) + (GravityDirection * time elapse^2)
You can decide in advance how far you pre calculate the points by simulating X duration and choosing the simulation step(calculate a point every X amount of time)
To detect collision each time you calculate a point you can do a small circle cast at that location. If it hits something you can stop add new points.
private float collisionCheckRadius = 0.1f;
private void SimulateArc()
{
float simulateForDuration = 5f;//simulate for 5 secs in the furture
float simulationStep = 0.1f;//Will add a point every 0.1 secs.
int steps = (int)(simulateForDuration/simulationStep);//50 in this example
List<Vector2> lineRendererPoints = new List<Vector2>();
Vector2 calculatedPosition;
Vector2 directionVector = new Vector2(0.5f,0.5f);//You plug you own direction here this is just an example
Vector2 launchPosition = Vector2.zero;//Position where you launch from
float launchSpeed = 10f;//Example speed per secs.
for(int i = 0; i < steps; ++i)
{
calculatedPosition = launchPosition + ( directionVector * (launchSpeed * i * simulationStep));
//Calculate gravity
calculatedPosition.y += Physics2D.gravity.y * ( i * simulationStep) * ( i * simulationStep);
lineRendererPoints.Add(calculatedPosition);
if(CheckForCollision(calculatedPosition))//if you hit something
{
break;//stop adding positions
}
}
//Assign all the positions to the line renderer.
}
private bool CheckForCollision(Vector2 position)
{
Collider2D[] hits = Physics2D.OverlapCircleAll(position, collisionCheckRadius);
if(hits.Length > 0)
{
//We hit something
//check if its a wall or seomthing
//if its a valid hit then return true
return true;
}
return false;
}
This is basically a sum of 2 vectors along the time.
You have your initial position (x0, y0), initial speed vector (x, y) and gravity vector (0, -9.81) being added along the time. You can build a function that gives you the position over time:
f(t) = (x0 + x*t, y0 + y*t - 9.81t²/2)
translating to Unity:
Vector2 positionInTime(float time, Vector2 initialPosition, Vector2 initialSpeed){
return initialPosition +
new Vector2(initialSpeed.x * t, initialSpeed.y * time - 4.905 * (time * time);
}
Now, choose a little delta time, say dt = 0.25.
Time | Position
0) 0.00 | f(0.00) = (x0, y0)
1) 0.25 | f(0.25) = (x1, y1)
2) 0.50 | f(0.50) = (x2, y2)
3) 0.75 | f(0.75) = (x3, y3)
4) 1.00 | f(1.00) = (x4, y4)
... | ...
Over time, you have a lot of points where the line will cross. Choose a time interval (say 3 seconds), evaluate all the points between 0 and 3 seconds (using f) and put your line renderer to cover one by one.
The line renderer have properties like width, width over time, color, etc. This is up to you.
I have just done the Curves and Splines Tutorials from the catlikecoding.com website (http://catlikecoding.com/unity/tutorials/curves-and-splines/) and after finishing it I came up with an idea. I would like to spawn the items from the SplineDecorator at the same distance from the spline at both sides of the spline. I have tried duplicating the for loop and instantiating the newItem twice but at different position but it doenst work as I want to. Here's the Spline Decorator script that takes a spline and then Instantiate some items along the its path
using UnityEngine;
public class SplineDecorator : MonoBehaviour
{
public BezierSpline spline;
public int frequency;
public float distanceBetweenItems;
public bool lookForward;
public Transform[] items;
private void Awake()
{
if (frequency <= 0 || items == null || items.Length == 0)
{
return;
}
float stepSize = 1f / (frequency * items.Length);
for (int p = 0, f = 0; f < frequency; f++)
{
for (int i = 0; i < items.Length; i++, p++)
{
Transform item = Instantiate(items[i]) as Transform;
Vector3 position = spline.GetPoint(p * stepSize);
item.transform.localPosition = new Vector3(position.x + distanceBetweenItems, position.y, position.z);
if (lookForward)
{
item.transform.LookAt(position + spline.GetDirection(p * stepSize));
}
item.transform.parent = transform;
}
}
}
}
You don't define exactly what "at both sides" means to you, so I'm not sure if this is what you're looking for, but maybe this will get you on the right track.
I replaced the inner loop with the following and got a sort of "race track" feel with distanceBetweenItems = 2:
Transform item = Instantiate(items[i]) as Transform;
Transform item2 = Instantiate(items[i]) as Transform;
Vector3 position = spline.GetPoint(p * stepSize);
Vector3 direction = spline.GetDirection(p * stepSize);
Vector3 cross = Vector3.Cross(direction, Vector3.up);
Vector3 delta = cross.normalized * (distanceBetweenItems/2);
item.transform.localPosition = new Vector3(position.x + delta.x, position.y + delta.y, position.z + delta.z);
item2.transform.localPosition = new Vector3(position.x - delta.x, position.y - delta.y, position.z - delta.z);
if (lookForward)
{
item.transform.LookAt( position + spline.GetDirection(p * stepSize));
item2.transform.LookAt(position + spline.GetDirection(p * stepSize));
}
item.transform.parent = transform;
item2.transform.parent = transform;
What I've done here is use Vector3.Cross() (Unity Documentation) to find the line perpendicular to both the spline and Vector3.up. Then I calculate how far along that line to go by normalizing it and multiplying it by half of distanceBetweenItems. (So that, combined, they're distanceBetweenItems apart.)
If you instead wanted something like tracing the wingtips of a plane flying that spline, you'll need to replace the Vector.up above. The simplest way is to replace it with the 'binormal vector', though there are issues with that. This pdf I just found talks a little about it, and might get you on the right path.
(Goofing around, I was able to get a reasonable approximation by replacing the Vector3 cross = ... line with Vector3 cross = Vector3.Cross(direction, direction - spline.GetDirection(p * stepSize + 0.01f));, though it's screwy on sharp bends, non-mirrored vertices, and at the start/end loop.)