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. 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
# 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_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() 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.
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 shaking
set_shake_translate(bool): enable or disable the translation of the camera when shaking
is_shake_rotate_enabled(): returns true if the rotation of the camera when shaking is enabled
is_shake_translate_enabled(): returns true if the translation of the camera when shaking is enabled
These 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,
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
Transform object is then created, using
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
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.
_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_to which tell starting and ending position of the "virtual pivot", as well as
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
_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.