As we finish up more and more of the combat art, some of our old techniques for controlling sprites from the Age of Temp Art have started to bother me. This afternoon I tackled one such issue dealing with the facing of our actor sprites.
When we first started out, we plugged in some representative fantasy character sprites as place holders for our characters so that we could prototype combat mechanics without waiting for an artist. (There are a lot of great resources on the web for this purpose. Check out
OpenGameArt.org or the
Spriters' Resource as a start.) Here are some examples:
While this was great for communicating character strengths or attack potential, it didn't communicate character facing! The side-on view in these pictures made it difficult to determine which of the 4 possible directions a character was facing in our 3-D world.
To address this at the time, I fudged the sprite's rotation in the world to hint at the direction it was facing. Here's an example using our latest sprites:
Unfortunately, rotating the sprite relative to the camera guarantees that some of the pixels will be blended together to make the final image that makes it to your screen. This means less visual fidelity for our otherwise crispy pixel art.
What I want instead is to have the character sprites always orient themselves in such a way that the plane of their sprite is parallel to the camera's view plane (effectively, the player's screen).
Unity's Transform class provides a handy function called
LookAt that gets us most of the way there. This function rotates the object's transform so that its forward vector points down the LookAt vector you provide, with an optional up vector hint. In our case, we tell each sprite to LookAt the camera's position. Here's the modified version:
If you compare this image with the previous one, you should notice that the sprites actually appear slightly larger, especially those near the center of the screen. The sprites are already much clearer, but there's still a slight problem.
Look closely at the three bandits at the top of that last image. Do they look a little different to you?
A naive application of that LookAt function I mentioned doesn't quite give us what we want. This is because our camera uses an orthographic projection instead of a normal perspective projection. (I won't go into this here, see
this for the gory details.) As a sprite approaches an edge of the screen, it rotates more and more to face the camera's position. Basically, sprites look skinnier as they approach the left or right side of the screen and shorter as they approach the top or bottom of the screen. Not good!
To fix this, we tell the sprite to LookAt different points on the camera's view plane such that the sprite is perfectly parallel to the screen. Here's the end result:
All three bandits are now the same size!
(Some of you will note that there are slight differences between the three sprites. Our combat rendering is not pixel perfect, so at times a sprite pixel will land on a screen pixel boundary. I may address this in a future post if I get time.)
I can then flip the sprite to give an indication of its facing, which won't affect the rotation of the sprite at all.
For those that are interested, here's a quick code snippet from the component responsible for orienting the sprites. Let me know if you have any questions or suggestions!
private void LateUpdate()
{
// Project the targets position onto the camera's xy-plane
// to get a look-at vector that is orthogonal to
// to the camera's xy-plane. This will ensure that each
// sprite is rotated at a similar angle, regardless of
// its position relative to the camera
// world position of the sprite
Vector3 target_position = m_cached_transform.position;
// world position of the camera
Vector3 camera_position = m_camera_transform.position;
// direction vector from the camera to the target
Vector3 camera_to_target = target_position - camera_position;
// project the camera_to_target vector onto the camera's up/down vectors
float target_up = Vector3.Dot(camera_to_target, m_camera_transform.up);
float target_right = Vector3.Dot(camera_to_target, m_camera_transform.right);
// determine the position on the camera's xy-plane such that
// (target_position - new_position).normalized == camera.forward
// the new look-at vector is orthogonal to the camera's xy-plane
Vector3 look_at_point = camera_position + target_up * m_camera_transform.up + target_right * m_camera_transform.right;
// finally rotate the target to face the newly computed look-at point
m_cached_transform.LookAt(look_at_point, m_camera_transform.up);
}