-->
The idea of the contents of the directory (keh_nodes
) is to hold addons that are pre-made nodes with attached scripts. Those can be directly used by simply adding a scene instance wherever the functionality is desired.
For the moment there is a single node:
The main objective of this node is to provide a "camera arm" that can be attached into a different node (the target) and keep the camera at an specified distance. The available features are:
To start using the Cam3D
all that is necessary is add a scene instance into the desired target object. Normally this will be attached into the player character. After that, the properties can be configured within the editor itself.
In this how to use you will find explanation for those properties up to the Max Shake Offset
, which is the last Cam3D
actual property. The rest is actually exposing the original Camera
properties so, if needed, you can change them.
Also, every single GDScript snippet will assume you have an instance of the Cam3D
named cam3d
. Maybe retrieved like this:
# some properties....
var cam3d: Cam3D
func _ready() -> void:
# Some code...
cam3d = $player_character/Cam3D
This should be an obvious property and it determines how far (or how close) the camera will be from the target object. This is in the Godot's 3D unit.
If you want to manipulate this value from GDScript, you can directly access the property arm_length
.
# Get the arm length of the Cam3D
var length1: float = cam3d.arm_length
# Set the arm length of the Cam3D
cam3d.arm_length = 10.5
This property is meant to prevent the camera from changing its orientation with the target. So, if you only keep Roll
enabled, the camera will change its pitch and its yaw when the target does so, however the camera roll will not change. Some example use cases:
X
. Then all 3 angles get locked. The camera will still follow the target but will never change its orientation with character, only if manually done through script.To manually change those values through script, you have to keep in mind that the property itself, lock_rotation
, is meant to be a bit mask, controlled by the flags FLR_ROLL
, FLR_PITCH
and FLR_YAW
. To facilitate here and avoid the necessity of having to deal with bit masking operations, there are several functions meant to deal with this property:
set_lock_pitch(bool)
: this allow you to enable or disable the pitch locking.set_lock_yaw(bool)
: this allow you to enable or disable the yaw locking.set_locK_roll(bool)
: this allow you to enable or disable the roll locking.is_pitch_locked()
: returns true if the pitch is locked.is_yaw_locked()
: returns true if the yaw is locked.is_roll_locked()
: returns true if the roll is locked.The camera can be configured to interpolate its transform when following the target. This is most useful when the target's visual representation is actually smoothed by interpolation. While you can individually enable/disable rotation and translation, ideally both should be at the same state. The reason those are separated goes from the fact that there was one bug on a different addon (network
) but I believed the culprit was caused by the interpolation method. Once the actual bug got fixed the camera's interpolation remained here.
To directly manipulate those settings from GDScript:
# Get the state of pivot interpolation
var is_interpolating_pivot: bool = cam3d.interpolate_pivot
# Get the state of orientation interpolation
var is_interpolating_orient: bool = cam3d.interpolate_orientation
# Enable interpolation on both translation and orientation
cam3d.interpolate_pivot = true
cam3d.interpolate_orientation = true
These two properties are directly connected, so dealing with them in a single topic.
The lag speed will be completely ignored if the lag is set to None
. From the drop down menu you can change the lag mode to Smooth Stop
or Smooth Start
. In either case the camera will have a certain delay behind the target, controlled by the Lag Speed
property. The lower this number, the more it will lag behind a moving target.
From GDScript you can change (or retrieve) those properties:
# Get current camera lag speed
var lag_speed: float = cam3d.lag_speed
# Set lag speed
cam3d.lag_speed = 0.01
# Get current lag mode
var lag_mode: int = cam3d.camera_lag
# Change to smooth start
cam3d.camera_lag = Cam3D.CameraLag.SmoothStart
# Or to smooth stop
cam3d.camera_lag = Cam3D.CameraLag.SmoothStop
# Or disable the camera lag
cam3d.camera_lag = Cam3D.CameraLag.None
This property determines how the camera will behave when something is detected between the camera and the target object. Note that currently this detection is done by a single ray cast and depending on the geometry of the objects it may miss something. Nevertheless, the available methods are:
None
: nothing will be done and objects will keep blocking the view.ShrinkArm
: the arm length will reduce its size so it's placed right in front of the blocking object. The length will be restored to its desired value when no object is blocking the path.CullObstructing
: from the first detected object until the camera, everything will be culled. This has the effect that some objects (ground for instance) will probably get partially culled.HideObstructing
: every detected object between the camera and the target will be hidden. Be careful with this mode in multiplayer games because hidden objects will not result in collisions for other objects.Which method you will use largely depends on the project and the desired effect. Nevertheless, to deal with that from GDScript:
# Get current collision mode
var coll_mode: int = cam3d.collision_mode
# Change collision mode to none
cam3d.collision_mode = Cam3D.CollisionMode.None
# Or to Shrink Arm
cam3d.collision_mode = Cam3D.CollisionMode.ShrinkArm
# Or to cull obstructing
cam3d.collision_mode = Cam3D.CollisionMode.CullObstructing
# Or to hide obstructing
cam3d.collision_mode = Cam3D.CollisionMode.HideObstructing
This property works exactly like the mask in the various collision shape objects. In other words, the enabled layers will be used to match objects. So, if you want to have some objects to be completely ignored by the collision checking, make sure those objects are part of a layer that is not enabled in this property.
In order to avoid having to deal with bit mask operations from code, two functions have been provided to deal with this aspect of the property, set_collision_layer()
and is_collision_layer_enabled()
.
The set_collision_layer()
allows one to enable or disable an specified collision layer index, matching the labels shown in the inspector. In other words, the index in here begins from 1 rather than 0.
Similarly, the is_collision_layer_enabled()
will return true if the specified layer index (again, starting from 1 rather than 0) is enabled.
The following snippet performs some silly manipulations:
# Disable collision Layer 2
cam3d.set_collision_layer(false, 2)
# Check if collision layer 5 is enabled and, if not, enabled it
if (!cam3d.is_collision_layer_enabled(5)):
cam3d.set_collision_layer(true, 5)
This is another bit mask property, that can be used to individually enable/disable the possible traumas that can be applied to the camera when shaking it. Specifically, the shake can change the rotation, the orientation or both. As with the rotation lock, there are some functions to avoid having to directly deal with the masks:
set_shake_rotate(bool)
: enable or disable the rotation of the camera when shakingset_shake_translate(bool)
: enable or disable the translation of the camera when shakingis_shake_rotate_enabled()
: returns true if the rotation of the camera when shaking is enabledis_shake_translate_enabled()
: returns true if the translation of the camera when shaking is enabledThese two properties determine how the shake will behave. The trauma decay indicate how fast it will stop shaking while the frequency how fast it will shake. Ok, things will probably get clearer with the explanation of how to use the shake system.
To make the camera shake you basically call the function add_trauma(amount)
. By calling this with a positive number the camera will then have some amount of trauma which will then cause it to shake during the update. At that point, trauma_decay
will be removed from the camera's trauma and when it reaches 0, it stops shaking. But the shake_frequency
determines how fast the camera will actually shake. So, one thing to keep in mind here is that the trauma is always kept in range [0 .. 1].
From GDScript, this snippet shows how to change those properties and then cause the camera to shake:
# Get current trauma decay
var current_decay: float = cam3d.trauma_decay
# Get current shake frequency
var current_frequency: float = cam3d.shake_frequency
# Change decay rate
cam3d.trauma_decay = 0.5
# Change frequency
cam3d.shake_frequency = 3.5
# Add maximum trauma
cam3d.add_trauma(1.0)
Each of these two properties is a Vector3
, giving more control over the shaking behavior. Each one specifies the maximum amount that each component can be changed while shaking. So, for example, if the MaxShakeOffset.x
is set to 2
, then the final X coordinate of the camera will be changed by something in between -2 and +2. The same applies to any other component.
Manipulating those from GDScript:
# Get current maximum shake rotation
var shake_rot: Vector3 = cam3d.max_shake_rotation
# Get current maximum shake translation
var shake_translation: Vector3 = cam3d.max_shake_offset
# Change the maximum rotation
cam3d.max_shake_rotation = Vector3(1.0, 1.5, 1.2)
# Change the maximum offset
cam3d.max_shake_offset = Vector3(2.0, 2.2, 2.5)
Obviously you can access each vector's component:
# Change just the maximum offset X when shaking
cam3d.max_shake_offset.x = 2.1
The first thing to be noted is that this script contains the tool
keyword near its top. The reason for this is to enable the script to be run within the editor. With this, changing the camera arm property will allow editor preview of the location of the camera in comparison to the target. This can be very useful when trying to decide the initial rotation of the camera's pivot (which basically means directly changing the rotation of the Cam3D instance within the editor).
Then note that this object derives from Position3D
and not Camera
. What happens here is that the position works as the control reference point to calculate the rest. The camera itself is attached to this position node. When the parent (target object) changes its global transform, so does the Cam3D
. This transform remains in that way but is used as reference to compute the rest. Basically a "virtual pivot point" is used and from it the camera is positioned.
If you take a look into the _process()
function, the first thing that is done is retrieve the pivot position and the desired camera orientation, which is based on the lock rotation settings. They way those two things are retrieved may not be that trivial to know though. So, let's delve into them. The most important thing to note here is the fact that those values are set based on the condition that the _interpolation
property is valid. It usually isn't within the editor. Nevertheless, the _interpolation
is an object that is used to perform the smooth movement of the camera, which will be explained shortly.
For the moment, let's focus on the two local variables, pivotpos
and cambasis
. Those two will be used to determine the transform that will be applied to the camera at the end of the _process()
function. So, suppose for a moment that there shouldn't be any interpolation in the movement nor any locked camera rotation. In that case, the pivotpos
is exactly equal to the global position of the Cam3D
itself, while the cambasis
corresponds to the orientation. The position of the camera (campos
) is then calculated by using the orientation, forward vector (-z of of the Basis), and moving it away from the pivot by arm_length
units.
A Transform
object is then created, using cambasis
and campos
as orientation and position. This is then applied to the global transform of the camera.
This should give the basic knowledge to understand the rest of the system of this Cam3D
addon, mostly because all that happens based on the settings is to perform some changes to the pivotpos
and cambasis
which, obviously, change the campos
. So let's first deal with the interpolation, which is the first check done in the _process()
function after the initial calculation. If the object is valid, then the script is most likely running in the game and not editor. This object holds necessary data to perform the actual interpolation of the camera position, so let's take a step back and check it.
The _interpolation
property is an instance of an inner class named InterpData
. In order to interpolate we need two reference values (starting and ending) and a "percent". Since this system can individually interpolate rotation and translation of the camera (the reason for this was explained in the how to use tab), there are four properties for those, pivot_from
/pivot_to
which tell starting and ending position of the "virtual pivot", as well as cam_rotfrom
/cam_rotto
which tell the starting and ending orientation of the camera. There are other two properties that will be explained shortly.
Ok, so the physical locations of the objects in the game are updated during the _physics_process()
function. But we use the _process()
function to update their visual representation (and camera). With that in mind, what is done within the physics is basically update a flag in the _interpolation
object indicating that there was a physics tick and the interpolation data must be updated. From both the physics and the normal process functions, if the flag is set, then the interpolation data is cycled through. During this "cycling", the two "old" final values are set to be the initial while the "current" transform is used to set the new ending values.
Going back to the _process()
function, if the interpolate_pivot
property is enabled, then the amount of time elapsed since the last update is used as interpolation alpha (percent, whatever you prefer) to calculate the interpolated pivotpos
. The same is done for cambasis
. If at least one of the two is changed, so does the campos
, which has to be recalculated.
We can go back to the last property of the InterpData
inner class, the cam_pos
. This property is meant to hold the most recent calculated camera position, which is used to incrementally update it to perform the camera lag feature. Speaking of that, the implementation here is directly based on the talk by Squirrel Eiserloh in his GDC talk called Juincing Your Camera With Math, which can be found here. In any case, the technique is called asymptotic averaging.
Later on in the function, after the camera's new global transform has been calculated and applied, the camera shake is then performed. Two conditions must be valid in order for this to be done. The first, and probably the most important is that the _shake
object must be valid and the second one is that there must be some trauma. Well, actually there is a third condition, and that is the shake to be enabled, which is individually checked.
But, wait, what is this _shake
object? Let's check it out. As with the interpolation data, there is this inner class ShakeState
meant to hold information related to the shaking. There are three properties there, noise
which is just an instance of OpenSimplexNoise
, which is used to smooth out the camera perturbation. Then trauma
which holds the amount of current trauma. And finally the time
, which is used calculate an "index" to retrieve data from the noise object.
At this point, supposing the _shake
object is valid and there is trauma, then the verification of which perturbations must be done is performed. Basically, the overall perturbation (delta angle or delta offset) consists of taking a value from the noise object and modifying it by the current trauma and maximum delta. Provided the rotation and or translation is enabled, the camera's transform is directly changed by the calculated deltas.
I have skipped one part of the _process()
function, which deals with the collision mode. Well, I have done that because this aspect of the Cam3D
has been divided between the _process()
and _physics_process()
. Basically in the _physics_process()
only the hide obstructing method is calculated, while all the others are left in the normal process function. Why did I do that? And most importantly, why did I "ignore" the fact that I'm casting rays outside of the physics process? Yes indeed, that must be explained.
The hide obstructing method is performed within the physics process as it normally should. A ray is cast from the pivot point towards the camera, if an object is found, that one get its visibility
property set to false
. Then another ray is cast and this process only ends when the ray reaches the camera without touching anything in between. The thing here is that this is changing the actual physics state of the game, in the sense that objects are becoming hidden and thus will not collide with anything else.
As for the other collision modes, before I explain the idea behind each, I obviously have to mention the reason I have taken the code from _physics_process()
to the _process()
. In order to cast rays, we need to access the direct space state, which the documentation says to not do so outside of the physics update. This is told because the state may be locked in a multi-threaded setup. Originally the code was indeed in the physics update, however the result was really weird. The thing is, because the camera's position is interpolated (and most likely the visual representation of the objects too), casting the ray from the actual physical location will not correspond to what is being seen. This becomes a lot more apparent when you drop the physics updates per second to lower value, like <= 30
, and the target moves from a position where a wall was obstructing the view to a "free view".
Because those casts are not being used to actually change the physical state, only the visual aspect of the camera, the possibility of moving the code to the normal process became almost necessary. Then I didn't find any mention about threads in 3D physics, so I moved the code from the physics function into the normal one and am currently wishing things don't completely break.
Nevertheless, the basic idea of the two other collision modes are:
Before I wrap this how it works, I want to talk about one last aspect, which is the _get_cam_basis()
function. It basically deals with the orientation of the camera based on the lock rotation settings. Basically, if nothing is locked the camera should be oriented according to the target object, which corresponds to the global transform of the Cam3D
. On the other side of the "spectrum", if all 3 axes are locked, then the camera orientation should correspond to the relative transform of the Cam3D
. With that in mind, when just a few are locked then a combination of global and relative transforms must be done, which is exactly what this function does.