My son asked to do some game programming with me last week. I was super excited. We did an eyeball rolling around in a circular motion (see image below). He did everything from working in Unity to making the assets and materials and I was very proud. I helped with the function to make it circle. It got me thinking how to explain circular movement and the use of Pythagorean triangles and cos and sin functions. They are simple maths but kind of hard to explain without a picture so hence this project about making circles with triangles.
The Basic Idea
To simplify I’ll work in 2D so we only got two Axis. X and Y. The Center of our circle will be on the 0 (zero, zero). So we could think of this as a co-ordinate system with two planes. If we were mathematicians we would call this a Cartesian Plane after Rene Descartes but being a Game Developer with Unity I’m going to call it the Scene view in 2D.
Our circle is defined by it’s Radius (ie. the distance from the Center).
On each Update() event our GameObject gets a new X,Y position on the Radius of the circle.
The Angle of Change
To start with we work out what the angle of difference is between this Update and the last one. The time passed during that period is known as the DeltaTime. Our “speed” is how many Radians we are travelling around the circle in that time. A radian is the length of the radius laid around the circumference of the circle. We multiply the Speed (how fast we are ticking through the radians) by the DeltaTime (time passed since last Update) to tell us that angle size.
# angleInRadians += RotateSpeed * Time.deltaTime;
Convert Polar to Cartesian
After working out the angle we have what is called a Polar Coordinate. That is we define our location by how far away it is (the distance – in this case as it’s a circle it’s always the same i.e. the radius), and what the angle (θ) is. Now we need to convert between that definition of a location in the Scene View to another one we can use in the Unity move function.
This is where Pythagoras and right angled triangles comes in – to convert from Polar Coordinates (r,θ) to Cartesian Coordinates (x,y) we use cos and sin :
x = r × cos( θ )
y = r × sin( θ )
In Unity there is already a function that does this:
newPosition = new Vector2(Mathf.Cos(angleInRadians), Mathf.Sin(angleInRadians)) * Radius;
But what does this really mean? Cos and Sin are just numbers. They represent the relationship between two sides of the triangle.
For example cos is the value of the relationship between the side of the triangle that is adjacent to the angle (the side next to the angle) and the hypotenuse (the long side on the other side of the angle). We are going to use cos to find the x value (how far along the horizon it is) in our Vector2 position. The same way we use Sin to find the y position (how far up or down).
In the image below see how there are squares built off each side of the triangle. Pythagorean theory for right angled triangles states that the area/volume of the two squares built of the two smaller sides will be equal to the area/volume of the big square built off the longest (hypotenuse) side of the triangle.
The big blue square has a volume of 1 because the radius of the circle is 1 unit on the Cartesian plane and One Squared is One. The volume of the other two squares (Magenta and Yellow) will always add up to one. Their volumes added together will always equal the volume of the blue square. This is the Pythagorean theory of right angled triangles in action. The length of a side of the Yellow box is the x value and the length of a side on the Magenta box is the y value. That’s our current position in (x, y) format which we can pass to the transform.position function.
The two images below show two Updates() that are reasonably close together so you can see in detail and in “freeze frame” what’s going on between the Updates() with all the variables. You can see the angle go from about 8 degrees to 20 degrees and the changing values for sin and cos which result in changing (x,y) values and volumes of the squares.
That’s basically it apart from some modifications to the values for being on the negative sides of the circle.
The script attached to the moving object is below but I’ve also put it on github here: https://github.com/zuluonezero/MoveInACircleWithTriangles
I’m finding it deeply satisfying to watch the triangles and squares get used to define a circle going round over and over again.
If this sort of thing floats your boat I’ve done some other posts on making curves using intersecting lines:
http://localhost/2019/06/04/unity-2d-curves-using-triangles/
Also on using sin to use curves in movement: http://localhost/2018/06/20/fly-birdy-fly-2d-curved-movement-in-unity/.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEditor;
public class CircleMe : MonoBehaviour
{
public float RotateSpeed = 5f; // How fast we move through the radians
public float Radius = 0.1f; // How "deep" the circle is
public Vector2 centreOfCircle; // The Centre of our circle
public float angleInRadians; // The angle (in radians) between our position between one update and the next
// A radian is the angle created if the length of the radius
// is laid along the circumference of the circle (about 57.2958 degrees)
public Vector2 newPosition; // The new position for every new Update event
public Text displayText;
public float angleInDegrees;
private LineRenderer triLine;
private Vector3 centre;
private Vector3 yLoc;
public float angle4Display;
private Vector3 sq1;
private Vector3 sq2;
private Vector3 sq3;
private Vector3 sq4cyan1;
private Vector3 sq4cyan2;
public Vector2 slopecyan;
public Vector2 p1cyan;
public Vector2 p2cyan;
private void Start()
{
centreOfCircle = transform.position;
// (0, 0) but could be anywhere
centre = transform.position;
}
private void Update()
{
angleInRadians += RotateSpeed * Time.deltaTime;
if (angleInRadians > 6.28319f)
{
angleInRadians = (angleInRadians % 6.28319f);
}
// eg. 0 += 5 * 0.25 (answer is 1.25) // if deltaTime was 0.25 of a second
// and our initial angle was 0 radians.
// Remember += is short for x = the current value of x plus itself (x = x + x)
// we need to convert the angle and radius into an x, y position
newPosition = new Vector2(Mathf.Cos(angleInRadians), Mathf.Sin(angleInRadians)) * Radius;
// = new Vector2(opposite/hypotenuse(1.25), adjacent/hypotenuse(1.25)) * Radius;
// (x, y) = (0.9997, 0.0218) * 0.1
// (x, y) = (0.09997, 0.00218)
transform.position = centreOfCircle + newPosition; // Adding Vectors
// (0.09997, 0.00218) = (0, 0) + (0.09997, 0.00218)
// this is our starting (x, y) position
// Now do it again for the next Update - the code below has been duplicated for this example
/*
angleInRadians += RotateSpeed * Time.deltaTime;
// eg. 1.25 += 5 * 0.25 (answer is now 1.25 + 1.25 = 2.5) // if deltaTime was 0.25 of a second
newPosition = new Vector2(Mathf.Cos(angleInRadians), Mathf.Sin(angleInRadians)) * Radius;
// = new Vector2(opposite/hypotenuse(2.5), adjacent/hypotenuse(2.5)) * Radius;
// (x, y) = (0.99904, 0.0436) * 0.1
// (x, y) = (0.09990, 0.00436)
transform.position = centreOfCircle + newPosition; // Adding Vectors
// (0.09990, 0.00436) = (0, 0) + (0.09990, 0.00436)
*/
if (transform.position.x > 0)
{
yLoc = new Vector3(centre.x + Radius, centre.y, centre.z);
}
else
{
yLoc = new Vector3(centre.x - Radius, centre.y, centre.z);
}
angleInDegrees = angleInRadians * 57.2958f;
}
public void OnDrawGizmos()
{
// Radius
Gizmos.color = Color.blue;
Gizmos.DrawWireSphere(centre, Radius);
// Yellow square
Gizmos.color = Color.yellow;
sq1 = new Vector3((transform.position.x / 2), (transform.position.x / 2f), 0f);
if (transform.position.x > 0)
{
if (transform.position.y > 0)
{
Gizmos.DrawWireCube(new Vector3((transform.position.x / 2), -(transform.position.x / 2f), 0f), new Vector3((transform.position.x), (transform.position.x), (transform.position.x)));
Handles.Label(new Vector3((transform.position.x / 2), -(transform.position.x / 2f), 0f), "Vol: " + (transform.position.x * transform.position.x));
}
else
{
Gizmos.DrawWireCube(sq1, new Vector3((transform.position.x), (transform.position.x), (transform.position.x)));
Handles.Label(sq1, "Vol: " + (transform.position.x * transform.position.x));
}
}
else
{
if (transform.position.y > 0)
{
Gizmos.DrawWireCube(sq1, new Vector3((transform.position.x), (transform.position.x), (transform.position.x)));
Handles.Label(sq1, "Vol: " + (transform.position.x * transform.position.x));
}
else
{
Gizmos.DrawWireCube(new Vector3((transform.position.x / 2), -(transform.position.x / 2f), 0f), new Vector3((transform.position.x), (transform.position.x), (transform.position.x)));
Handles.Label(new Vector3((transform.position.x / 2), -(transform.position.x / 2f), 0f), "Vol: " + (transform.position.x * transform.position.x));
}
}
// Magenta square
Gizmos.color = Color.magenta;
sq2 = new Vector3((transform.position.y / 2), (transform.position.y / 2f), 0f);
if (transform.position.x > 0)
{
if (transform.position.y > 0)
{
Gizmos.DrawWireCube(new Vector3((transform.position.x + transform.position.y / 2), (transform.position.y / 2f), 0f), new Vector3((transform.position.y), (transform.position.y), (transform.position.y)));
Handles.Label(new Vector3((transform.position.x + transform.position.y / 2), (transform.position.y / 2f), 0f), "Vol: " + (transform.position.y * transform.position.y));
}
else
{
Gizmos.DrawWireCube(new Vector3((transform.position.x + Mathf.Abs(transform.position.y) / 2), (transform.position.y / 2f), 0f), new Vector3((transform.position.y), (transform.position.y), (transform.position.y)));
Handles.Label(new Vector3((transform.position.x + Mathf.Abs(transform.position.y) / 2), (transform.position.y / 2f), 0f), "Vol: " + (transform.position.y * transform.position.y));
}
}
else
{
if (transform.position.y > 0)
{
Gizmos.DrawWireCube(new Vector3((transform.position.x - transform.position.y / 2), (transform.position.y / 2f), 0f), new Vector3((transform.position.y), (transform.position.y), (transform.position.y)));
Handles.Label(new Vector3((transform.position.x - transform.position.y / 2), (transform.position.y / 2f), 0f), "Vol: " + (transform.position.y * transform.position.y));
}
else
{
Gizmos.DrawWireCube(new Vector3((transform.position.x + transform.position.y / 2), (transform.position.y / 2f), 0f), new Vector3((transform.position.y), (transform.position.y), (transform.position.y)));
Handles.Label(new Vector3((transform.position.x + transform.position.y / 2), (transform.position.y / 2f), 0f), "Vol: " + (transform.position.y * transform.position.y));
}
}
// Red Triangle
Gizmos.color = Color.red;
Gizmos.DrawLine(centre, new Vector3(transform.position.x, centre.y, centre.z));
Gizmos.DrawLine(new Vector3(transform.position.x, centre.y, centre.z), transform.position);
Gizmos.DrawLine(transform.position, centre);
if (transform.position.x > 0)
{
if (transform.position.y > 0)
{
Gizmos.DrawLine(new Vector3(transform.position.x - 0.1f, 0f, 0f), new Vector3(transform.position.x - 0.1f, 0.1f, 0f));
Gizmos.DrawLine(new Vector3(transform.position.x - 0.1f, 0.1f, 0f), new Vector3(transform.position.x, 0.1f, 0f));
}
else
{
Gizmos.DrawLine(new Vector3(transform.position.x - 0.1f, 0f, 0f), new Vector3(transform.position.x - 0.1f, -0.1f, 0f));
Gizmos.DrawLine(new Vector3(transform.position.x - 0.1f, -0.1f, 0f), new Vector3(transform.position.x, -0.1f, 0f));
}
}
else
{
if (transform.position.y > 0)
{
Gizmos.DrawLine(new Vector3(transform.position.x + 0.1f, 0f, 0f), new Vector3(transform.position.x + 0.1f, 0.1f, 0f));
Gizmos.DrawLine(new Vector3(transform.position.x + 0.1f, 0.1f, 0f), new Vector3(transform.position.x, 0.1f, 0f));
}
else
{
Gizmos.DrawLine(new Vector3(transform.position.x + 0.1f, 0f, 0f), new Vector3(transform.position.x + 0.1f, -0.1f, 0f));
Gizmos.DrawLine(new Vector3(transform.position.x + 0.1f, -0.1f, 0f), new Vector3(transform.position.x, -0.1f, 0f));
}
}
// Cyan Square
Gizmos.color = Color.cyan;
if ((transform.position.x > 0 && transform.position.y > 0) || (transform.position.x < 0 && transform.position.y < 0))
{
slopecyan = new Vector2(transform.position.x, transform.position.y);
p1cyan = new Vector2((transform.position.x - slopecyan.y), (transform.position.y + slopecyan.x));
p2cyan = new Vector2((centre.x - slopecyan.y), (centre.y + slopecyan.x));
Gizmos.DrawLine(transform.position, p1cyan);
Gizmos.DrawLine(centre, p2cyan);
Gizmos.DrawLine(p1cyan, p2cyan);
}
else
{
slopecyan = new Vector2(transform.position.x, transform.position.y);
p1cyan = new Vector2((transform.position.x + slopecyan.y), (transform.position.y - slopecyan.x));
p2cyan = new Vector2((centre.x + slopecyan.y), (centre.y - slopecyan.x));
Gizmos.DrawLine(transform.position, p1cyan);
Gizmos.DrawLine(centre, p2cyan);
Gizmos.DrawLine(p1cyan, p2cyan);
}
Vector3 lbl = new Vector3((p1cyan.x / 2), (p1cyan.y / 2), 0f);
Handles.Label((lbl), "Vol: " + (Radius * Radius));
// Angle Marker
if (transform.position.y > 0)
{
if (transform.position.x > 0)
{
angle4Display = angleInDegrees;
Handles.DrawSolidArc(centre, Vector3.forward, yLoc, angle4Display, 0.25f);
}
else
{
angle4Display = -(angleInDegrees - 180f);
Handles.DrawSolidArc(centre, Vector3.forward, transform.position, angle4Display, 0.25f);
}
}
else
{
if (transform.position.x < 0)
{
angle4Display = (angleInDegrees - 180f);
Handles.DrawSolidArc(centre, Vector3.forward, yLoc, angle4Display, 0.25f);
}
else
{
angle4Display = -(angleInDegrees - 360f);
Handles.DrawSolidArc(centre, Vector3.forward, transform.position, angle4Display, 0.25f);
}
}
// Labels
Handles.color = Color.blue;
Handles.Label(centreOfCircle, angle4Display.ToString());
Handles.color = Color.white;
Handles.Label(transform.position, "X: " + System.Math.Round(transform.position.x, 2) + " Y: " + System.Math.Round(transform.position.y, 2));
// sin opposite/hypotenuse
Handles.Label(new Vector3(1.2f, 0.8f, 0f), "sin opposite/hypotenuse");
Handles.Label(new Vector3(1.3f, 0.7f, 0f), "sin: " + Vector3.Distance(centre, (new Vector3(transform.position.y, 0f, 0f))) / Vector3.Distance(centre, (transform.position)) );
// cos adjacient/hypotenuse
Handles.Label(new Vector3(1.2f, 0.6f, 0f), "cos adjacient/hypotenuse");
Handles.Label(new Vector3(1.3f, 0.5f, 0f), "cos: " + Vector3.Distance(centre, (new Vector3(transform.position.x, 0f, 0f))) / Vector3.Distance(centre, (transform.position)));
// tan opposite/adjacient
Handles.Label(new Vector3(1.2f, 0.4f, 0f), "tan opposite/adjacient");
Handles.Label(new Vector3(1.3f, 0.3f, 0f), "tan: " + Vector3.Distance(centre, (new Vector3(transform.position.y, 0f, 0f))) / Vector3.Distance(centre, (new Vector3(transform.position.x, 0f, 0f))));
Handles.Label(new Vector3(1f, -0.3f, 0f), "Next Position on Update()");
Handles.Label(new Vector3(1f, -0.4f, 0f), "newPosition = new Vector2(Mathf.Cos(angleInRadians), Mathf.Sin(angleInRadians)) * Radius");
Handles.Label(new Vector3(1f, -0.5f, 0f), "= new Vector2(opposite/hypotenuse(angleInRadians), adjacent/hypotenuse(angleInRadians)) * Radius");
Handles.Label(new Vector3(1f, -0.6f, 0f), "" + Mathf.Cos(angleInRadians) + ", " + Mathf.Sin(angleInRadians) + " * " + Radius + " = " + newPosition);
}
}
/*
Using Cartesian Coordinates we mark a point by how far along (x) and how far up (y) it is:
Using Polar Coordinates we mark a point by how far away (magnitude or in this case as it's a circle always the radius is the same), and what angle (θ) it is:
To convert from Polar Coordinates (r,θ) to Cartesian Coordinates (x,y) :
x = r × cos( θ )
y = r × sin( θ )
Example: add the vectors a = (8,13) and b = (26,7)
c=a+b
c= (8,13) + (26,7) = (8+26,13+7) = (34,20)
*/