Retro FPS-Style Object Angling Tutorial
(Sorry for the weird title. As far as I know, there is no common name for this effect!)
Old FPS games like Doom did that thing where the sprites had multiple angles, and the angle shown was dependent on where the sprite was viewed from. Most of these games used 8 angles for the sprites, but I will show you how to write the code in a way that allows you to use any number of angles. I would provide a repo with code, but I'd rather explain how the code works so you can fit it into your game according to your needs.
First, we need to set a variable that stores the number of viewing angles our object will be visible from. I made this a constant, but if you want to make the number of angles different on a per-object basis, make it an export variable instead. I find that multiples of 8 work the best.
const ROTATION_ANGLES := 8
Now, we need to calculate the angle increment and store it as a variable, which is the number of degrees that the viewing angle needs to change by before the object renders from a different angle.
var angle_increment := 360.0/float(ROTATION_ANGLES)
For the next part, I highly recommend getting a reference to the node that represents the object we want to do this effect on and storing it in a variable.
onready var object = $"Insert node path to your object here"
Next, in the
_process() function, we need to calculate the viewing angle relative to the camera's position and the object's forward axis. This is calculated with:
var theta = angle_wrap(atan2(target.x, target.z) - atan2(forward.x, forward.z))
targetis the normalized difference between the camera's position and the object's position. This can be calculated with
((camera.translation - object.translation) * Vector3(1, 0, 1)).normalized()We multiply by
Vector3(1, 0, 1)because Retro FPS games did not take the Y-axis of the player into account.
forwardis the forward axis of the object we are viewing. You can get this from your object with
object.get_global_transform().basis.z. This is already normalized, so no need to normalize it yourself.
thetawill be in radians.
angle_wrap()is a function I made that adds 2Pi if
thetais less than 0, and subtracts 2Pi if
thetais greater than 2Pi.
Now that we have
theta, we need to figure out which "angle index" this value corresponds to. An index of 0 represents the angles at which the object should be shown facing the camera. From there, every angle increment going counter-clockwise increases the index by one, up until a maximum of the number of angles minus one. In the case having 8 viewing angles, this would mean that the index ranges from 0 to 7, and increases every 45 degrees.
To find the angle index, I made this function:
func get_angle_index(theta:float) -> int: var min_angle := 0.0 var max_angle := angle_increment * 0.5 for x in range(ROTATION_ANGLES): if theta >= min_angle and theta < max_angle: return x min_angle = max_angle max_angle = min_angle + angle_increment return 0
This function loops through all the rotation angles, and checks to see if theta is in between the values that correspond to that angle index, returning that index when it finds that range of values. This function works regardless of whether
theta is in degrees or radians, but keep in mind what units
theta is in when using the value this returns.
At this point, all we need to do to get the angle index is to use:
var angle_index = get_angle_index(theta)
Now that we have a way to get the angle index, we can use it to display our object from a certain angle! The way this is used is heavily dependent on how your game is set up, so here are some usage examples:
Use with Sprites
You can use Godot's Sprite3D node to place a 2D image in a 3D world. To get the same effect that many modern applications using this effect use, set the Sprite's Billboard property to "Y-Billboard".
If you use sprites, you'll need to have a version of each sprite facing every possible angle index. Early FPS games would often mirror the right-facing sprites to left, reducing the number of sprites that needed to be made.
Use with 3D Models
You can use the angle index to create a psuedo-sprite effect using a 3D model, allowing you to retain the flexibility offered by a model which a sprite does not. I did this by first setting up a scene arranged like this:
- Enemy (Sprite3D)
- Orbit (Spatial)
The Sprite3D gets its texture from its child Viewport, which renders a model that cannot be seen by the player's camera. This can be accomplished either by placing the Mesh on its own visual layer, or by setting the Viewport's "Own World" property to true. Enabling Own World, however, will also cause the viewport to use its own lighting!
Orbit is a Spatial node located at Mesh's origin. The camera Orbit is parented to is offset along the positive Z axis. This allows the camera to show the model from a different angle when Orbit is rotated. To rotate the Camera so that it would display the model's angle index, I set Orbit's rotation like this:
orbit.rotation_degrees.y = angle_increment * get_angle_index(theta)
Lastly, the RemoteTransform has its "Remote Path" property set to the Mesh. This is necessary because the mesh being a child of the viewport causes it to not update when the Enemy node is moved or rotated. The RemoteTransform solves this issue.
The end result is that the 3D model will look like a prerendered sprite! You can make this look even more retro by lowering the resolution of the viewport, and by adding shaders to your model.
Edit 1: Fixed typos, added info about using a RemoteTransform for the 3D example.