_Concealed Intent is on Steam Greenlight! Please vote for it here
I’ll be GDC this year. If you are there too and watch to meet up, feel free to send me a message.
In Concealed Intent I wanted enemies to circle the players, and not just in a predictable flat y=? plane. No, instead they should fly all sorts of curved paths above and below their targets. The game is fully 3D and the enemy AI should demonstrate this. So began my affair with Unity3D’s Transform.RotateAround. If that function and its uses are second nature to you then this blog post will be old news, if not then read on…
So what exactly does this Transform.RotateAround(Vector3 point,
Vector3 axis, float angle);
function do? It moves the
Transform
through angle
degrees around the
point
given. That is, point
is the center of a
circle with the Transform
on its edge, which moves
angle
around this circle.
So what does the axis
vector do? In maths, it is the
normal of the
plane on which the circle is drawn. Imagine walking on flat ground
in a circle and looking straight up into the sky, that is the
axis
vector. Now imagine walking in a circle on the side of
a steep hill and looking up directly away from the ground. You are no
longer looking straight up into the sky, but at an angle because the
circle in which you are walking on is itself on an angle. The “up”
vector points away from the slope, and again this is the
axis
. The circle the sun makes as it crosses the sky would
have the axis
as Vector3.right
or
Vector3.left
, depending on which way around the circle you
wanted it to move.
Now let’s say you wanted to have a spaceship move around another object
(its target) in a circle. Although in space it doesn’t matter which way
is up as long as the ship continues in a nice circle. So you have a
Transform
for the ship and a vector for the target, what is
“up”. The axis
could be fixed as Vector3.up
or
Vector3.right
, but then the enemy ships will always circle
their targets in the same way - too easy to predict. Instead, each ship
should work out its own “up”, but a random vector by itself can’t be
used.
The point
and the axis
parameters define a
plane. If the Transform
is not in this plane then rather
than rotate around the point
, Unity will shift the plane
along the axis
(“up”) until it is in the plane and then
rotate around that point. This is shown in the image above. This
situation maybe what is desired. Imagine a camera looking down on a
player from an angle. To get this camera to rotate around the player
while remaining at the same height use
cameraTransform.RotateAround(playerPosition, Vector3.up,
angle)
(assuming Vector3.up
is the right axis for
the game).
If this shifting of the center of the circle is not what is required (as
in Concealed Intent) then the axis
provided must define a
plane that includes the Transform
. Luckily there is a handy
way to calculate such an axis - the cross
product, already provided
in Unity by
Vector3.Cross.
Cross
takes two vectors and uses them to define a plane,
returning the normal of that plane. Since we know the
Transform
to rotate and the point
it should
rotate around, one of these vectors is Transform.position -
point
. The other vector can be anything, so I just use a random
vector. The code I use to get the axis
is below, where
Me
is the the enemy, and center
is the point
it is circling:
do {
Vector3 perturb = new Vector3(Random.Range(-1, 1), Random.Range(-1, 1), Random.Range(-1, 1)).normalized;
normal = Vector3.Cross(me.Position - center, perturb).normalized;
bool newOrientation = Vector3.Dot(normal, Vector3.up) > 0;
if (orientation != newOrientation) {
normal = -normal;
}
} while (count++ < RETRIES && normal.magnitude < 0.01f);
Two extra points to note here. First is that if the vector
Transform.position - point
is parallel to the random
vector, then the cross product won’t work (it will be the zero vector).
This is the reason for the check normal.magnitude \< 0.01f
and recalculating the normal if this is true
. A number
slightly greater than 0 is used to handle any numerical errors in the
calculation. The second
line to note is Vector3.Dot(normal, Vector3.up) \> 0;
-
this checks that the normal always points in expected direction and thus
always goes clockwise or counterclockwise depending on the value of
orientation
.
It can also sometimes be useful not to use a random vector when
crossing. If you know something special about the situation a specific
second vector may be appropriate. For instance, if the direction in
which the target is moving is known this could be used as the vector to
cross with. Then depending on the orientation of the resulting normal,
the Transform
will either rotate towards the direction or
movement, or away from it.
Now with the appropriate axis, Transform.RotateAround(Vector3
point, Vector3 axis, float angle);
can be called to get the
expected result. However, Concealed Intent does two extra things. Below
is the code it uses:
Vector3[] CreateCircle(Vector3 self, Vector3 center, Vector3 up, float radius) {
Vector3[] circle = new Vector3[steps];
dummyTrans.position = self;
for (int i = 0; i < circle.Length; i++) {
float ratio = (i + 1) / (float)steps;
dummyTrans.RotateAround(center, up, 180*ratio);
circle[i] = dummyTrans.position * (1+(radius-1)*ratio);
}
return circle;
}
Concealed Intent is turn based - it needs to know the enemy ships’ plans
before they actually move. Thus rather than call
RotateAround
on the enemy Transform
itself, it
uses a dummy transform on an empty Gameobject
instead and
stores its position at various (steps
) points in the
circle. Also, the enemy may want to spiral into, or away from, the
target. This is achieved with the … \*
(1+(radius-1)\*ratio)
, if radius is less than 1 it will spiral
in; greater than 1 it will spiral away.
So that is nearly all I’ve learnt about moving objects in circles in Unity. RotateAround is a powerful tool, use it wisely.
Contact Details: