How to Spawn Objects in Unity3D with a Minimum Distance between - c#

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

Related

I am attempting to make game objects spawn at random points along the Y-Axis

I am attempting to make a flappybird style game that is underwater with the PC dodging mines and collecting fish to score points. The issue that I am having is that the mines spawn off screen as intended and fly across the screen, however, they spawn in a straight line. I am following a tutorial as I dont know C# so I am practicing to get knowledge up. But in doing so I am not sure where I am going wrong and google searched yielded no solution.
This is the code for spawning in the mines
private void HandleMineSpawning() {
Timer -= Time.deltaTime;
if (Timer < 0) {
Timer += TimerMax;
float heightEdge = 10f;
float minHeight = offset + heightEdge;
float totalHeight = camSize * 2;
float maxHeight = totalHeight - offset *.5f - heightEdge;
float height = Random.Range(minHeight, maxHeight);
CreateMineOffset(height, offset, mineSpawn);
}
And the code that should create an offset on the Y-axis
private void Awake() {
minelist = new List<Mine>();
TimerMax = 1.5f;
offset = 20;
}
private void CreateMineOffset(float offsetY, float offsetSize, float xPosition) {
CreateMine(offsetY - offsetSize * .5f, xPosition);
CreateMine(camSize * 2f - offsetY - offsetSize * .5f, xPosition);
}
This was written for a 3d game but I am sure you can modify it for a platformer game.
Set the y spread to a high value and set the x to a low value. The Z spread can stay at zero for your game.
Hope this helps.
public GameObject itemsToSpread;
GameObject spawn;
public int numberOfItemsToSpawn;
public float Space = 10;//Distance between game objects
//Offset values go here
public float itemXSpread = 5;
public float itemYSpread = 100;
public float itemZSpread = 0;//Add value here for a 3d distribution
// Start is called before the first frame update
void Start()
{
for (int i = 0; i < numberOfItemsToSpawn; i++)
{
SpreadItem();
}
}
void SpreadItem()
{
Vector3 ranPos = new Vector3(Random.Range(-itemXSpread, itemXSpread) + Space, Random.Range(-itemYSpread, itemYSpread) + Space, Random.Range(-itemZSpread, itemZSpread) + Space) + transform.position;
spawn = Instantiate(itemsToSpread, ranPos, Quaternion.identity);
}
Output:
For your game, try these values.

My Game's Infinite Platform Generation does Not Work

I have followed a YouTube tutorial on how to make a doodle jump replica in 5 minutes.
The problem is, this tutorial is rushed and does not provide all the information needed and thus I have encountered one big problem.
The infinite random platform generation keeps skewing off to either the left or right side of the screen after a certain point, and I have no clue as to why.
Here is the video tutorial: https://www.youtube.com/watch?v=IUzI95mmbwA
I am just wondering if anyone on here could be kind enough to help me on this small problem as it is for my school project and I am not sure how to fix it.
GameManager:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
public GameObject platformPrefab;
public int platformCount = 300;
// Use this for initialization
void Start ()
{
Vector3 spawnPosition = new Vector3();
for (int i = 0; i < platformCount; i++)
{
spawnPosition.y += Random.Range(.5f, 2f);
spawnPosition.x += Random.Range(-5f, 5f);
Instantiate(platformPrefab, spawnPosition, Quaternion.identity);
}
}
}
Platforms being close together issue
So you generate your platforms using
void Start ()
{
Vector3 spawnPosition = new Vector3();
for (int i = 0; i < platformCount; i++)
{
spawnPosition.y += Random.Range(.5f, 2f);
spawnPosition.x += Random.Range(-5f, 5f);
Instantiate(platformPrefab, spawnPosition, Quaternion.identity);
}
}
which basically means that the next platform always has an offset to the one before in a range +/- 5.
This means however, that the next value would always have to counterbalance the current offset in order to be fully balanced in total. Since you use a random and not an equal distribution thiis is not guaranteed!
In order to make sure platforms don't get "skewed" to one side infinitely I would simply make sure they still "fit into the screen" and clamp the position accordingly like e.g.
public class GameManager : MonoBehaviour
{
public GameObject platformPrefab;
public int platformCount = 300;
[SerializeField] private Camera mainCamera;
// Adjust via the Inspector in order to also take platform width into account!
public float platformWidth;
[Min(0)] public float xRange = 5f;
float minX;
float maxX;
// Use this for initialization
void Start ()
{
if(!mainCamera) mainCamera = Camera.main;
// get the screen boarders in world space
minX = mainCamera.ScreenToWorldPoint(new Vector3(0, 0, -mainCamera.transform.position.z)).x + platformWidth / 2f;
maxX = mainCamera.ScreenToWorldPoint(new Vector3(Screen.width, 0, -mainCamera.transform.position.z)).x - platformWidth / 2f;
Vector3 spawnPosition = Vector3.zero;
for (int i = 0; i < platformCount; i++)
{
spawnPosition.y += Random.Range(.5f, 2f);
spawnPosition.x += Random.Range(-xRange, xRange);
// now simply clamp the x position so the platform
// doesn't go over the screen boarders
spawnPosition.x = Mathf.Clamp(spawnPosition.x, minX, maxX);
Instantiate(platformPrefab, spawnPosition, Quaternion.identity);
}
}
}
You could of course also instead of only clamping the position actively counter balance the range and shift it left or right accordingly like e.g.
Vector3 spawnPosition = Vector3.zero;
for (int i = 0; i < platformCount; i++)
{
spawnPosition.y += Random.Range(.5f, 2f);
var min = -5f;
var max = 5f;
var distanceToLeftBorder = spawnPosition.x - minX;
var distanceToRightBorder = maxX - spawnPosition.x;
if(distanceToLeftBorder < xRange)
{
min += xRange - distanceToLeftBorder;
max += xRange - distanceToLeftBorder;
}
else if(distanceToRightBorder < xRange)
{
min -= xRange - distanceToRightBorder;
max -= xRange - distanceToRightBorder;
}
spawnPosition.x += Random.Range(min, max);
// now simply clamp the x position so the platform
// doesn't go over the screen boarders
spawnPosition.x = Mathf.Clamp(spawnPosition.x, minX, maxX);
Instantiate(platformPrefab, spawnPosition, Quaternion.identity);
}
To your final issue the platforms being too close I would take yet another different approach:
Instead of randomly place at offset +/- 5 rather separate the sign from the distance and do
var sign = Mathf.Sign(Random.value);
var distance = Random.Range(platformWidth + SPACING, maxDistance);
spawnPosition.x += distance * sign;
which would make sure that the next platform is at minimum the platform width plus an optional spacing (both positive!) and not any closer to the current platform.
I would then instead of clamping simply check if you would hit the border and switch the sign like
var sign = Mathf.Sign(Random.value);
var distance = Random.Range(platformWidth + SPACING, maxDistance);
if(spawnPosition.x + distance * sign < minX || spawnPosition.x + distance * sign > maxX)
{
sign *= -1;
}
spawnPosition.x += distance * sign;

2D Projectile Trajectory Prediction(unity 2D)

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.

How can I use multiple enums (two enums) to control two axis the object will rotate at the same time?

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Rotate : MonoBehaviour
{
public enum Axistorotate { Back, Down, Forward, Left, Right, Up, Zero };
public Vector3[] vectorAxises = new Vector3[7];
public Axistorotate[] myAxis;
public float angle;
public float speed;
private bool stopRotation = true;
// Start is called before the first frame update
void Start()
{
vectorAxises[0] = Vector3.back;
vectorAxises[1] = Vector3.down;
vectorAxises[2] = Vector3.forward;
vectorAxises[3] = Vector3.left;
vectorAxises[4] = Vector3.right;
vectorAxises[5] = Vector3.up;
vectorAxises[6] = Vector3.zero;
StartCoroutine(RotateObject());
}
public Vector3 GetAxis(Axistorotate axis)
{
return vectorAxises[(int)axis];
}
// Update is called once per frame
void Update()
{
if(Input.GetKeyDown(KeyCode.S))
{
stopRotation = false;
}
if (Input.GetKeyDown(KeyCode.C) && stopRotation == false)
{
stopRotation = true;
StartCoroutine(RotateObject());
}
}
IEnumerator RotateObject()
{
while (stopRotation == true)
{
for (int i = 0; i < myAxis.Length; i++)
{
transform.Rotate(GetAxis(myAxis[i]), angle);
}
yield return new WaitForSeconds(speed);
}
}
}
For some reason while the game is running and I'm changing one of the enums to forward or back or down each time it's rotating another direction. If I'm starting when both enums on back states and then changing one of them to down it looks like it's rotating to the left or right and then when changing back to back it's not rotating like it was when they were both set to back.
How can I update the vectorAxises array in real time while the game is running to show in the Inspector the current axis state for example : vectorAxises[0] -> 1, 0, -1 ..... vectorAxises[7] -> 0,-1, 0 I want that when I'm changing one of the enums that it will show it on the vectorAxises.
Maybe I need to create another vectorAxises array one for each enum ?
And maybe when doing two myAxis it's changing the same one the same angle so it's not realy two enums that change individual axis ?
transform.Rotate has an optional parameter
relativeTo
Determines whether to rotate the GameObject either locally to the GameObject or relative to the Scene in world space.
which by default is Space.Self.
So your
transform.Rotate(GetAxis(myAxis[i]), angle);
is always done in the local coordinate system of the GameObject. This local system is rotated along with the GameObject so the local transform.up, transform.forward etc axis change all the time.
Instead make it rotate around world axis
transform.Rotate(GetAxis(myAxis[i]), angle, Space.World);
How can I update the vectorAxises array in real time while the game is running to show in the Inspector the current axis state
This should already be the case. Or do you mean you want to see the currently "selected" value. You should use Debugging and Breakpoints for that. Since you do
for (int i = 0; i < myAxis.Length; i++)
{
transform.Rotate(GetAxis(myAxis[i]), angle);
}
without any further yield return the object will directly "jump" into the new rotation and in the inspector you would always only see the last GetAxis(myAxis[i]).
If you are looking for a smooth rotation then checkout Dest's answer slightly modified you could e.g. let the object rotate within 1 second
while (stopRotation == true)
{
// calculate the target rotation
Quaternion rotationQuaternion = Quaternion.identity;
for (int i = 0; i < myAxis.Length; i++)
{
rotationQuaternion *= Quaternion.AngleAxis(angle, GetAxis(myAxis[i]));
}
// before starting to rotate store initial and target rotation
var initialRotation = transform.rotation;
var targetRotation = initialRotation * rotationQuaternion;
// could also get this from the Inspector e.g.
var rotationDuration = 1;
// Do a smooth rotation from the initial to target rotation
// within the defined rotationDuration
var timePassed = 0f;
do
{
// additionally ease-in and -out the rotation
var lerpFactor = Mathf.SmoothStep(0, 1, timePassed / rotationDuration);
transform.rotation = Quaternion.Slerp(initialRotation, targetRotation, lerpFactor);
timePassed += Time.deltaTime;
// let this state be rendered
yield return null;
} while(timePassed < rotationDuration);
// if you still want the pause
yield return new WaitForSeconds(speed);
}
Just out of curiosity: Why even use an enum here? Couldn't you directly iterate through the vectorAxises index instead and only add those entries you will be using?
Use Quaternions for multiple rotations at the same time:
Quaternion rotationQuaternion = Quaternion.identity;
for (int i = 0; i < myAxis.Length; i++)
{
rotationQuaternion *= Quaternion.AngleAxis(angle, GetAxis(myAxis[i]));
}
transform.rotation *= rotationQuaternion;
It should fix your problem with wrong rotations

Acceleration in Unity

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

Categories