I'm trying to make a projectile which has movement behaviour shown as red in the diagram.
What I know and have now is two vectors; Start and End.
The end goal is to have some randomness of the arc at iteration and projectile velocity change in a lerp-fashion. I've done linear movement generations before, but nothing like this.
If my question feels like asking you to do my work for me (My usual fear of asking questions as a novice coder) could I have some tips and hints on what methods/commands I should look into? The language is C# and Unity version is 5.6
EDIT # 1
After getting some head-direction I could achieve something closer to the end-goal function of this.
Blue linear line is just representation of distance and angle between A(initiation point) and B(target). The red arc is the trajectory I'm willing to make my projectile to move as.
Fortunately, I figured what I wanted my path to guide the projectile to follow was a cubic bezier and got the result in editor shown in the diagram above with A, B, modA, and modB. There are just a few more things I need to get working on actually mounting the projectile to follow this path and control its velocity and etc. Following are more questions which I couldn't get through today.
First, the general condition is A is fixed and B is not. In order to maintain the generally desired flight path, I figured I need another virtual line lineB(from modA to modB) to sync lineA's angle and distance so when B(the target) is moving around in all directions the arc is not too extremely skewed, but in my attempts today either I got the wrong angle from lineA or something modA just orbited around A and the numbers were weird like angle changing in straight line movement of B from A.
Second is to have some random-but-similar variety of the red arc after the first projectile fires and to the next. I'm guessing this would be somewhat easier when I get past the first one since it's just matter of controlling lineB.
Edit # 2
All the functions asked above are resolved: A path is generated from A to B with arc made with modA and modB as well as the randomness of modA and modB at each iteration as well as modA and modB adjusting according to B's position in real time.
Now All that's left is to actually make the projectile follow the path and control its velocity till reaching B. Below is the code generating the arc-path. How should I approach this?
public Transform[] controlPoints = new Transform[4];
public LineRenderer lineRenderer;
private int curveCount = 0;
private int SEGMENT_COUNT = 50;
private void DrawCurve()
{
for (int j = 0; j < curveCount; j++)
{
for (int i = 1; i <= SEGMENT_COUNT; i++)
{
float t = i / (float)SEGMENT_COUNT;
int nodeIndex = j * 3;
Vector3 pixel = CalculateCubicBezierPoint(
t,
controlPoints[nodeIndex].position,
controlPoints[nodeIndex + 1].position,
controlPoints[nodeIndex + 2].position,
controlPoints[nodeIndex + 3].position);
lineRenderer.positionCount = (((j * SEGMENT_COUNT) + i));
lineRenderer.SetPosition((j * SEGMENT_COUNT) + (i - 1), pixel);
}
}
}
private Vector3 CalculateCubicBezierPoint(float t, Vector3 start, Vector3 modA, Vector3 modB, Vector3 end)
{
float u = 1 - t;
float t2 = Mathf.Pow(t, 2);
float u2 = Mathf.Pow(u, 2);
float t3 = Mathf.Pow(t, 3);
float u3 = Mathf.Pow(u, 3);
Vector3 p = u3 * start;
p += 3 * u2 * t * modA;
p += 3 * u * t2 * modB;
p += t3 * end;
return p;
}
You should use AnimationCurve.
you can edit the "graphic curve" in the inspector (public variable AnimationCurve) then use this srcipt to move object along the path.
using UnityEngine;
using System.Collections;
public class AnimationPath : MonoBehaviour
{
public AnimationCurve XCurve;
public float TotalTravelTime = 5.0f;
public float TravelSpeed = 50.0f;
public float XRange = 10.0f;
// Use this for initialization
void Start ()
{
StartCoroutine("Travel");
}
IEnumerator Travel()
{
float ElapsedTime = 0.0f;
while(ElapsedTime < TotalTravelTime)
{
float XPos = XCurve.Evaluate(ElapsedTime/TotalTravelTime) * XRange;
transform.position = new Vector3(XPos, transform.position.y, transform.position.z + TravelSpeed * -Time.deltaTime);
yield return null;
ElapsedTime += Time.deltaTime;
}
}
}
I hope this can help you.
Related
This question already has answers here:
Moving from one place to another over time (or rotating) in Unity
(2 answers)
Create a coroutine to fade out different types of object
(2 answers)
Closed 2 years ago.
I want to make the object change its position in a few seconds. For example, it will "reach" a point within 5 seconds.
Choose a position and reach it within a fixed number of time. It is possible? Does it have a function?
Lat example. My object is in (10f, 10f, 0) I want him to reach (20f, 10f, 0) in 2 seconds.
Is it possible?
Update: in scratch, for example it called "glide 10, 78. secs to 3" - moving to 10X, 78Y in 3 seconds
Unless you're 90 years old, just use a tweeng.
It's this easy
StartCoroutine(5f.Tweeng( (p)=>transform.position=p,
startHere,
endHere) );
it goes from startHere to endHere in 5 seconds.
That's all there is to it.
A tweeng extension is only a few lines of code, example: https://stackoverflow.com/a/37228628/294884
You can do anything with a tweeng, fade volume, twist objects, animate the size of type, fade images, whatever.
Every single large project you'll ever work on, they already use tweeng. It couldn't be simpler.
Use Vector3.Lerp inside a coroutine.
Vector3 beginPos = new Vector3(10f, 10f, 0);
Vector3 endPos = new Vector3(20f, 10f, 0);
float time = 2;
void Start(){
StartCoroutine(Move(beginPos, endPos, time));
}
IEnumerator Move(Vector3 beginPos, Vector3 endPos, float time){
for(float t = 0; t < 1; t += Time.deltaTime / time){
transform.position = Vector3.Lerp(beginPos, endPos, t);
yield return null;
}
}
Vector3.Lerp basically interpolates between two vectors:
Lerp(a, b, t) = (1-t)a + tb
Lerp(a, b, 0) = (1-0)a + 0b = a
Lerp(a, b, 0.5) = (1-0.5)a + 0.5b = 0.5a + 0.5b
Lerp(a, b, 1) = (1-1)a + 1b = b
To give you a general idea:
public class MoveToTarget : MonoBehaviour
{
public Transform target;
public float duration;
float distance;
float t;
private void Start()
{
distance = Vector3.Distance(transform.position, target.position);
}
private void Update()
{
transform.position = Vector3.MoveTowards(transform.position, target.position,
(distance/duration) * Time.deltaTime);
}
}
For sure!
There are a lot of ways to accomplish this in Unity.
I am going to assume that you are making a 3D experience and using RigidBody components.
FixedUpdate is a special method that Unity calls every 20ms - no matter what is going on with rendering, or performance - this is so you can do you physics simulations reliably and smoothly.
Remember the classic formula: rate * time = distance?
private void UpdateRigidBody()
{
Vector3 targetDestination = Vector3.MoveTowards(transform.position, destination, speed * Time.deltaTime);
var rb = transform.GetComponent<Rigidbody>();
rb.MovePosition(destination);
}
Where speed is calculated and with destination set on the object (below).
In your case, you know the distance (destination - current position), and you know the time = 2 seconds.
Your unknown is the rate (speed).
Speed = distance / time.
So you will need some other function to setup the movement - like ChaseTarget?
private void ChaseTarget(Vector3 target, float timeToReach)
{
destination = target;
var distance = (destination - transform.position);
speed = distance / timeToReach;
}
So, somewhere in your game controls logic you would call ChaseTarget and give it a target (a Vector3) and a time to get there. You setup these member variables on your object (targetDestination and speed) and the UpdateRigidBody function will smoothly get you there.
Typically, when you are close enough to the target you do some sort of new game logic and then reset the target destination (inside of UpdateRigidBody:
var destXZ = destination;
var posXZ = transform.position;
var dir = destXZ - posXZ;
var arrived = dir.magnitude < arrivedRadiusMagnitude;
if (arrived) // If I have arrived
{
Arrived();
}
Where the arrivedRadiusMagnitude is a game design question of how close can something get before it is considered arrived? For example some sort of tracking missile might explode when it is near a target, not necessarily right on top of it.
Ok, so, i've been stuck on this for ages. Im working on an AI that will navigate a tank to a waypoint, defined as a Vector3. the position of the tank is also defines as a Vector3, both these have their Y position set to 0, as to ignore terrain elevation, the current rotation of the tank is also a Vector3, though only the Y rotation is needed, as i'm effectively projecting the 3d position onto a 2d navigational grid.
The AI passes anywhere between -1 and 1 into the control for the tank, which then handles the physics operations. so, i need to somehow calculate the angle, positive or negative in relation to the current heading angle of the tank to the position of the waypoint, then send the rotation value to the controls. At the moment I simply cant get it working, I feel like iv'e pretty much tried everything.
This is my code currently, it doesn't work, at all, and is about the 20th revision:
void driveToTarget()
{
Vector3 target0 = driveTarget;
target0.y = 0;
GameObject current0Obj = new GameObject();
Vector3 current0 = this.transform.position;
current0.y = 0;
print(current0);
print(target0);
Vector3 current0Angle = this.transform.eulerAngles;
print(current0Angle.y);
current0Angle.x = 0;
current0Angle.z = 0;
Vector3 heading = target0 - current0;
Quaternion headingAngle = Quaternion.LookRotation(heading);
print("headingAngle" + headingAngle);
print("heading direction, allegidly: " + Quaternion.Euler(heading).ToEulerAngles());
Quaternion distanceToGo = Quaternion.Lerp(Quaternion.Euler(current0Angle), headingAngle, 0.01f);
float angle = Vector3.SignedAngle(current0, target0, Vector3.up);
float difference = Mathf.Abs(angle - current0Angle.y);
print("heading angle " + angle);
if (current0 != driveTarget)
{
steeringVal = Mathf.Abs(1.5f-(1f/Mathf.Abs(distanceToGo.y))) * -Mathf.Sign(distanceToGo.y); ;
throttleVal = 0f;
} else
{
throttleVal = 0;
}
}
--EDIT--
So, I've partially solved it, and now encountered another problem, I've managded to get the tank to detect the angle between itself and the waypoint, BUT, rather than orienting forward towards the waypoint, the right side of the tank orients towards it, so it orbits the waypoint. I actually know why this is, becasue the forward vector of the tank is technically the right vector because of unity's stupid axis ruining my blender import, anyway, heres the updated code:
void driveToTarget()
{
Vector3 target0 = driveTarget;
target0.y = 0;
Vector3 current0 = this.transform.position;
current0.y = 0;
print("Current: " + current0);
print("Target: " + target0);
Vector3 current0Angle = this.transform.rotation.eulerAngles;
print("Curret rotation:" + current0Angle.y);
current0Angle.x = 0;
current0Angle.z = 0;
Vector3 heading = target0 - current0;
Quaternion headingAngle = Quaternion.LookRotation(heading);
print("heading angle: " + headingAngle.ToEuler());
float distanceToGo = (current0Angle.y) - headingAngle.eulerAngles.y;
print("DistanceToGo: " + distanceToGo);
if (current0 != driveTarget)
{
steeringVal = 1 * -Mathf.Sign(distanceToGo);
throttleVal = 0f;
} else
{
throttleVal = 0;
}
Debug.DrawRay(current0, heading, Color.red, 1);
Debug.DrawRay(current0, this.transform.up, Color.red, 1);
}
I'm not sure exactly how your code is setup or how the steering works. You may want to look into using the Unity NavMeshAgent to simplify this.
Regardless here is some code I wrote up that takes a destination and rotates an object towards it. All you'd have to do from there is move the object forwards.
Vector3 nextDestination = //destination;
Vector3 direction = nextDestination - transform.position;
direction = new Vector3(direction.x, 0, direction.z);
var newRotation = Quaternion.LookRotation(direction);
var finalRotation = Quaternion.Slerp(transform.rotation, newRotation, Time.deltaTime); //smoothes out rotation
transform.rotation = finalRotation;
Sorry if this isn't what you needed. Have you been able to figure out which part of the code is behaving unexpectedly from your print statements?
Good day,
I'd like to program a constantly moving ball (object3) being passed between two stationary objects (object1, object2), with the ability to set the max height Y of the pass trajectory dynamically.
What would you argue is the best way to program the ball physics for this concept?
I've looked at using addForce on a default sphere w/ a rigidbody. It seems like there should be an equation that expresses the trajectory of a pass of object3 from object1's x to object2's x... at a known, given speed, with a known, set mass, and a known gravity environment.
However, currently I have a Vector3.Lerp interpolating the ball between the two objects on each FixedUpdate() with t expressed as:
`(Mathf.Sin(speed * Time.time) + 1.0f) / 2.0f;`
It works and all, but with this approach, it seems there's no clear way to add height to the trajectory of the ball path. I've considered adding the height to the Y value in object2 until the ball is half way there, and then setting it back to the original Y position... but it just feels wrong! Thoughts?
Thanks!
Okey so if I understand you correctly currently you are doing
privte void FixedUpdate()
{
var factor = (Mathf.Sin(speed * Time.time) + 1.0f) / 2.0f;
object1.MovePosition(Vector3.Lerp(object2.position, object3.position, factor));
}
which moves the ball pingpong between object1 and object2 position but only planar.
Assuming for now the objects will only be moving within the XZ plane and never have different Y position in order to rather get a curve with height you could treat the separatly:
- Interpolate between both positions as before
- Separately calculate the Y position with sinus or any other mathematical curve function - for realistic physics probably rather a parabola actually
Could look somhow like
public class Example : MonoBehaviour
{
public Rigidbody object1;
public Transform object2;
public Transform object3;
// adjust in the Inspector
public float speed = 1;
public float Amplitude = 0;
// Just for debug
[Range(0, 1)] [SerializeField] private float linearFactor;
[SerializeField] private float yPosition;
private void FixedUpdate()
{
// This always returns a value between 0 and 1
// and linearly pingpongs forth and back
linearFactor = Mathf.PingPong(Time.time * speed, 1);
// * Mathf.PI => gives now a value 0 - PI
// so sinus returns correctly 0 - 1 (no need for +1 and /2 anymore)
// then simply multiply by the desired amplitude
var sinus = Mathf.Sin(linearFactor * Mathf.PI);
yPosition = sinus * Amplitude;
// As before interpolate between the positions
// later we will ignore/replace the Y component
var position = Vector3.Lerp(object2.position, object3.position, linearFactor);
object1.MovePosition(new Vector3(position.x, yPosition, position.z));
}
}
You could optionally also try and add some dumping in the Y direction in order to make the vertical movement more realistic (slow down when reaching the peak). I tried a bit using inverted SmoothStep like
// just for debug
[Range(0, 1)] [SerializeField] private float dampedSinusFactor;
[Range(0, 1)] [SerializeField] private float linearFactor;
[SerializeField] private float yPosition;
private void FixedUpdate()
{
// Use two different factros:
// - a linear one for movement in XZ
// - a smoothed one for movement in Y (in order to slow down when reaching the peak ;) )
linearFactor = Mathf.PingPong(Time.time * speed, 1);
dampedSinusFactor = InvertSmoothStep(linearFactor);
// * Mathf.PI => gives now a value 0 - PI
// so sinus returns correctly 0 - 1 ()
// then simply multiply by the desired amplitude
var sinus = Mathf.Sin(dampedSinusFactor * Mathf.PI);
yPosition = sinus * Amplitude;
// later we will ignore/replace the Y component
var position = Vector3.Lerp(object2.position, object3.position, linearFactor);
object1.position = new Vector3(position.x, yPosition, position.z);
}
// source: https://stackoverflow.com/a/34576808/7111561
private float InvertSmoothStep(float x)
{
return x + (x - (x * x * (3.0f - 2.0f * x)));
}
However for slow movements this looks a bit strange yet. But you can come up with any other maths curve that results in the expected behavior for x=[0,1] ;)
Is there a build-in way how to get a time by value from Animation curve in Unity3d? (The opposite way of Evaluate)
I need to achieve this (instead of getting value from time):
float time = AnimationCurve.Evaluate(float value);
Generically speaking getting X value from Y value.
I know this is 3 years old, but I found via a Google search, and in case someone else lands here:
I simply create an inverse curve, which allows me to look up by time.
public AnimationCurve speedCurve;
private AnimationCurve inverseSpeedCurve;
private void Start()
{
//create inverse speedcurve
inverseSpeedCurve = new AnimationCurve();
for (int i = 0; i < speedCurve.length; i++)
{
Keyframe inverseKey = new Keyframe(speedCurve.keys[i].value, speedCurve.keys[i].time);
inverseSpeedCurve.AddKey(inverseKey);
}
}
Just a basic implementation maybe it will give an idea for you. Method loops through all time and if your value is near that value at that time it will yield. It's a coroutine but you can change it to use inside Update maybe?
public AnimationCurve curve;
public float valToTime = .5f;
public float treshold = .005f;
public float yourTime;
IEnumerator valueToTime(float determineTime)
{
float timeCounter = 0;
Keyframe[] k = curve.keys;
float endTime = k[k.Length-1].time;
Debug.Log("end "+endTime);
while(timeCounter < endTime)
{
float val = curve.Evaluate(timeCounter);
Debug.Log("val "+ val + " time "+timeCounter);
// have to find a better solution for treshold sometimes it misses(use Update?)!
if(Mathf.Abs(val - determineTime) < treshold)
{
//Your time would be this
yourTime = timeCounter;
yield break;
}
else
{
//If it's -1 than a problem occured, try changing treshold
yourTime = -1f;
}
timeCounter += Time.deltaTime;
yield return null;
}
}
Putting together the best elements of most of the solutions posted, I've come up with an approach that produces pretty high accuracy. It involves doing the work upfront and so, is also quite efficient.
Note: If the original curve possesses any maximum/minimum point (points on the curve with a gradient of zero) this method will still attempt to invert it but can only do so by introducing several discontinuities to the inverted curve. It is not ideal for such cases.
Evaluate the original curve at several "sample-points" using a "sample-delta" constant.
For each "value" evaluated, compute the tangent at that point as the "sample-delta" / "value-delta".
Create keyframes that use the "value" as the "time" and the "sample-point" as the "value", and set the "inTangent" and "outTangent" to the tangent obtained in Step 3.
Add the keyframe generated at every "sample-point" to a new AnimationCurve().
The new AnimationCurve() is therefore an inverted version of the original.
Smooth the tangents of the new AnimationCurve() (the inverted version) to remove discontinuities caused by sudden and rapid tangent changes. NB: Smoothing the tangents may make the inverted curve lose it's general definition if the original curve had at least one maximum/minimum point.
Image of Normal Curve vs Inverted Curve:
invertedCurve = new AnimationCurve();
float totalTime = normalCurve.keys[normalCurve.length - 1].time;
float sampleX = 0; //The "sample-point"
float deltaX = 0.01f; //The "sample-delta"
float lastY = normalCurve.Evaluate(sampleX);
while (sampleX < totalTime)
{
float y = normalCurve.Evaluate(sampleX); //The "value"
float deltaY = y - lastY; //The "value-delta"
float tangent = deltaX / deltaY;
Keyframe invertedKey = new Keyframe(y, sampleX, tangent, tangent);
invertedCurve.AddKey(invertedKey);
sampleX += deltaX;
lastY = y;
}
for(int i = 0; i < invertedCurve.length; i++)
{
invertedCurve.SmoothTangents(i, 0.1f);
}
I needed this very thing just now, so I came up with this. I found it quite accurate and fast (an accuracy value of 10 was enough, and even lower may have done). But it will only work on curves that have ONE definite time for each value (i.e. nothing like waves with multiple times having the same value).
Similar to the other answer, it iterates through possible times - but rather than in a linear fashion the step value starts as the entire time range and halves each time.
Hope it's useful for you.
// NB. Will only work for curves with one definite time for each value
public float GetCurveTimeForValue( AnimationCurve curveToCheck, float value, int accuracy ) {
float startTime = curveToCheck.keys [0].time;
float endTime = curveToCheck.keys [curveToCheck.length - 1].time;
float nearestTime = startTime;
float step = endTime - startTime;
for (int i = 0; i < accuracy; i++) {
float valueAtNearestTime = curveToCheck.Evaluate (nearestTime);
float distanceToValueAtNearestTime = Mathf.Abs (value - valueAtNearestTime);
float timeToCompare = nearestTime + step;
float valueAtTimeToCompare = curveToCheck.Evaluate (timeToCompare);
float distanceToValueAtTimeToCompare = Mathf.Abs (value - valueAtTimeToCompare);
if (distanceToValueAtTimeToCompare < distanceToValueAtNearestTime) {
nearestTime = timeToCompare;
valueAtNearestTime = valueAtTimeToCompare;
}
step = Mathf.Abs(step * 0.5f) * Mathf.Sign(value-valueAtNearestTime);
}
return nearestTime;
}
just stumbled upon this problem myself and didn't like the solutions mentioned here, so i wanted to share my own. It's rather an adaption to the answer which inverts the keyframes.
I improved it by also inverting the tangents and the weight of the points.
I'm sure there is an easier way, but i found this working nicely for reversing the animationcurve.
Edit: Forgot to mention, for me it only worked when the tangents are set to weighted, i don't know what weight calculation unity does when you set it to auto or similar, so weighted was predicatable and easy to inverse.
inverseCurve = new AnimationCurve();
for (int i = 0; i < initialCurve.length; i++)
{
float inWeight = (initialCurve.keys[i].inTangent * initialCurve.keys[i].inWeight) / 1;
float outWeight = (initialCurve.keys[i].outTangent * initialCurve.keys[i].outWeight) / 1;
Keyframe inverseKey = new Keyframe(initialCurve.keys[i].value, initialCurve.keys[i].time, 1/initialCurve.keys[i].inTangent, 1/initialCurve.keys[i].outTangent, inWeight, outWeight);
inverseCurve.AddKey(inverseKey);
}
Thought I'd share my own version, as suggested in other forums too I tried looping over Evaluate() instead of reversing the whole curve which I think is overkill and not always feasible.
This checks for a value approximation down to the indicated decimals, it also assumes that the curve has "normalized" time (if it wasn't the case this could be expanded by looking for the smallest and the biggest time keys.
/// <summary>
/// Inverse of Evaluate()
/// </summary>
/// <param name="curve">normalized AnimationCurve (time goes from 0 to 1)</param>
/// <param name="value">value to search</param>
/// <returns>time at which we have the closest value not exceeding it</returns>
public static float EvaluateTime(this AnimationCurve curve, float value, int decimals = 6) {
// Retrieve the closest decimal and then go down
float time = 0.1f;
float step = 0.1f;
float evaluate = curve.Evaluate(time);
while(decimals > 0) {
// Loop until we pass our value
while(evaluate < value) {
time += step;
evaluate = curve.Evaluate(time);
}
// Go one step back and increase precision of the step by one decimal
time -= step;
evaluate = curve.Evaluate(time);
step /= 10f;
decimals--;
}
return time;
}
How to draw circle in Unity 3d?
I want to draw a circle around different objects.
The radiuses of the circles are different and the circles have textures - squares.
I found a big error with this code. The number of points (Size) shouldn't be "(2 * pi / theta_scale) + 1" because this causes the circle to draw 6.28 times. The size should be "1 / theta_scale + 1". So for a theta_scale of 0.01 it needs to draw 100 points, and for a theta_scale of 0.1 it needs to draw 10 points. Otherwise it would draw 62 times and 628 times respectively.
Here is the code I used.
using UnityEngine;
using System.Collections;
public class DrawRadar: MonoBehaviour {
public float ThetaScale = 0.01f;
public float radius = 3f;
private int Size;
private LineRenderer LineDrawer;
private float Theta = 0f;
void Start() {
LineDrawer = GetComponent<LineRenderer>();
}
void Update() {
Theta = 0f;
Size = (int)((1f / ThetaScale) + 1f);
LineDrawer.SetVertexCount(Size);
for (int i = 0; i < Size; i++) {
Theta += (2.0f * Mathf.PI * ThetaScale);
float x = radius * Mathf.Cos(Theta);
float y = radius * Mathf.Sin(Theta);
LineDrawer.SetPosition(i, new Vector3(x, y, 0));
}
}
}
If you modify the number in "Size" that is divided by ThetaScale, you can make a sweeping gauge/pie chart type graphic.
See Unity Answers for a similar question.
Alternatively:
float theta_scale = 0.1; // Circle resolution
LineRenderer lineRenderer = gameObject.AddComponent<LineRenderer>();
lineRenderer.material = new Material(Shader.Find("Particles/Additive"));
lineRenderer.SetColors(c1, c2);
lineRenderer.SetWidth(0.2F, 0.2F);
lineRenderer.SetVertexCount(size);
int i = 0;
for(float theta = 0; theta < 2 * PI; theta += theta_scale) {
x = r*cos(theta);
y = r*sin(theta);
Vector3 pos = new Vector3(x, y, 0);
lineRenderer.SetPosition(i, pos);
i+=1;
}
The LineRenderer requires continuous points. You can modify this code slightly to use cylinder game objects instead of a line renderer. I find the LineRenderer to be a bit hideous.
Lastly, similar to the first link, you could attach a circle texture to a unit plane. Make any part of the texture that isn't part of the circle transparent. Then just scale and align the plane to fit your object. Unfortunately this method isn't great if someone is looking almost parallel to the plane.
Jerdak's solution is good, but the code is messy so I had to tweak a little. Here's the code for a class, where I use i in the loop to avoid a bug.
It also updates the circle's position with its gameObject position.
using UnityEngine;
using System.Collections;
public class CircleDraw : MonoBehaviour {
float theta_scale = 0.01f; //Set lower to add more points
int size; //Total number of points in circle
float radius = 3f;
LineRenderer lineRenderer;
void Awake () {
float sizeValue = (2.0f * Mathf.PI) / theta_scale;
size = (int)sizeValue;
size++;
lineRenderer = gameObject.AddComponent<LineRenderer>();
lineRenderer.material = new Material(Shader.Find("Particles/Additive"));
lineRenderer.SetWidth(0.02f, 0.02f); //thickness of line
lineRenderer.SetVertexCount(size);
}
void Update () {
Vector3 pos;
float theta = 0f;
for(int i = 0; i < size; i++){
theta += (2.0f * Mathf.PI * theta_scale);
float x = radius * Mathf.Cos(theta);
float y = radius * Mathf.Sin(theta);
x += gameObject.transform.position.x;
y += gameObject.transform.position.y;
pos = new Vector3(x, y, 0);
lineRenderer.SetPosition(i, pos);
}
}
}
Using Shader Graph we can now draw pixel perfect circle.
Once you created this graph, create a new material based on this shader.
Then create a new gameobject with a sprite renderer and set the material you just created.
You can scale the circle using the "scale" parameter of the material.
The linerenderer method in the top answers is really simple and exactly what I was looking for. I updated it for newer versions of Unity and some small tweaks to make it a bit more beginner/user friendly.
Specifically:
LineRenderer.SetVertexCount() is deprecated in newer versions of Unity, replaced with positionCount
Replaced theta scale with an actual segment count to remove guesswork
Added loop setting - not sure if this was in older versions of Unity, it can be set in the LineRenderer's inspector
Removed unnecessary Update function - the rendered line is a persistent gameobject
using UnityEngine;
[RequireComponent(typeof(LineRenderer))]
public class DrawRing : MonoBehaviour
{
public LineRenderer lineRenderer;
[Range(6,60)] //creates a slider - more than 60 is hard to notice
public int lineCount; //more lines = smoother ring
public float radius;
public float width;
void Start()
{
lineRenderer = GetComponent<LineRenderer>();
lineRenderer.loop = true;
Draw();
}
void Draw() //Only need to draw when something changes
{
lineRenderer.positionCount = lineCount;
lineRenderer.startWidth = width;
float theta = (2f * Mathf.PI) / lineCount; //find radians per segment
float angle = 0;
for (int i = 0; i < lineCount; i++)
{
float x = radius * Mathf.Cos(angle);
float y = radius * Mathf.Sin(angle);
lineRenderer.SetPosition(i, new Vector3(x, 0, y));
//switch 0 and y for 2D games
angle += theta;
}
}
}
Note this is assumed to be attached to the gameobject you want the ring around. So the Use World Space option in LineRenderer should be unchecked. Also remember that the scale of the gameobject will affect the position of the points and the width of the line.
To put this on the ground (as in a unit selection circle):
Put the script on a separate gameobject
Rotate the gameobject X to 90
Check use world space on the linerenderer
Set the linerenderer Alignment to Transform Z
Add the position of the thing you want to circle to x and y in SetPosition. Possibly along with replacing 0 with 0.1f or a yOffset variable to avoid z-fighting with terrain.
Circle can draw using shader - draw pixel if it on radius from center.
Did the following with a Sprite. Chan is flying in the scene, so she's slightly above the plane. I had her flying so I could get a good screenshot, not because it wouldn't play well with the plane.
I used a low-resolution circle sprite.
X rotation 90
Scale X 15, Y 15, Z 1
Then I set the Sorting Layer, so it will render above the Default Layer. I was testing this out when I came across this post. It doesn't handle shadows well. I'd have to figure out what layer shadows are drawn on to make sure they get rendered onto the sprite.
I have a shader from which I usually start making effects like lens flares, and it makes a circle. Using shader is the best choice because you will get perfectly smooth and round circle.
Also it's easy to experiment with and tune the shader since shader changes don't require recompile and re-entering of play mode.
I recommend ti create extension method to GameObject. Worked good to me.
public static class GameObjectExtension
{
const int numberOfSegments = 360;
public static void DrawCircle(this GameObject go, float radius,
float lineWidth, Color startColor, Color endColor, bool lineRendererExists=true)
{
LineRenderer circle = lineRendererExists ? go.GetComponent<LineRenderer>() : go.AddComponent<LineRenderer>();
circle.useWorldSpace = false;
circle.startWidth = lineWidth;
circle.endWidth = lineWidth;
circle.endColor = endColor;
circle.startColor = startColor;
circle.positionCount = numberOfSegments + 1;
Vector3 [] points = new Vector3[numberOfSegments + 1];
for (int i = 0; i < numberOfSegments + 1; i++)
{
float rad = Mathf.Deg2Rad * i;
points[i] = new Vector3(Mathf.Sin(rad) * radius, 0, Mathf.Cos(rad) * radius);
}
circle.SetPositions(points);
}
}
One More thing to note: If LineRenderer component is not applied last parameter has to be false
create a static class to reuse the code for different game objects. player, enemies... when the class is static, you cannot create the instance of it
public static class CircleGameObject
{
// in static class methods have to be static as well
// "this" refers to the context that we are calling DrawCircle
public static async void DrawCircle(this GameObject container,float radius,float lineWidth)
{
// I provide 360 points because circle is 360 degrees and we will connect them with line
var segments=360;
// LineRenderer is used to draw line
var lineRenderer=container.AddComponent<LineRenderer>();
// now you can use position system relative to the parent game object.
lineRenderer.useWorldSpace=false;
lineRenderer.startWidth=lineWidth;
lineRenderer.endWidth=lineWidth;
lineRenderer.positionCount=segments+1;
// reserve empty array in memory with a size of lineRenderer.positionCount
var points=new Vector3[lineRenderer.positionCount];
// draw all of those points
for(int i=0;i<points.Length;i++)
{
// converting degree to radian because Mathf.Cos and Mathf.Sin expects radian
var radian=Mathf.Deg2Rad*i;
// y direction needs to be 0
// Mathf.Cos(radiant) will give the x position on the circle if the angle size is "radian"
// Mathf.Sin(radiant) will give the y position on the circle if the angle size is "radian"
// after for loop completes we would be getting 360 points
points[i]=new Vector3(Mathf.Cos(radian)*radius,0,Mathf.Sin(radian)*radius);
}
lineRenderer.SetPositions(points);
}
}
then call it in Awake of the context
public class PlayerController : MonoBehaviour
{
private void Awake()
{
GameObject go=new GameObject{
name="Circle"
};
Vector3 circlePosition=Vector3.zero;
go.transform.parent=transform;
// localPosition is relative to the parent
go.transform.localPosition=circlePosition;
go.DrawCircle(2.0f,0.03f);
....
}
}