185 lines
10 KiB
Markdown
185 lines
10 KiB
Markdown
|
# godot-smoother-node
|
||
|
|
||
|
A **Godot 4** node type that smoothes scene node movements by interpolating `_physics_process` steps.
|
||
|
|
||
|
![godot-smoother-node-comparison](https://user-images.githubusercontent.com/7110246/209624079-86824089-444d-4f6e-bd02-b2b38e3952c4.gif)
|
||
|
|
||
|
Above: Not smoothed vs. smoothed.
|
||
|
|
||
|
## *Smoother* node
|
||
|
|
||
|
This node interpolates properties of other nodes between their `_physics_process`es. The interpolation is applied in the `_process` loop which ensures that nodes move smoothly, even if the `_physics_process` is called less often than the games fps rate which is typically synced to the current screen's refresh rate.
|
||
|
|
||
|
By default only the node `position` is interpolated.
|
||
|
|
||
|
Visit [godot-smoother-node-test-scene](https://github.com/anatolbogun/godot-smoother-node-test-scene) to download a Godot 4 sample project.
|
||
|
|
||
|
### YouTube Tutorial
|
||
|
|
||
|
[![Youtube Tutorial](https://user-images.githubusercontent.com/7110246/209792804-f471d454-2d0a-487f-8599-46ef0af0ea5e.png)](https://www.youtube.com/watch?v=jIkPYlNF50Q)
|
||
|
|
||
|
|
||
|
### Usage
|
||
|
|
||
|
#### Basic Usage
|
||
|
|
||
|
Add the [smoother.gd](https://github.com/anatolbogun/godot-smoother-node/blob/main/smoother.gd) script to your project. Since it has a `class_name` it is automatically added to the available nodes of the project.
|
||
|
|
||
|
Simply add the *Smoother* node as a child to your root node (like a level root). By default it will interpolate the `position` of all supported[^1] and relevant[^2] nodes in the scene.
|
||
|
|
||
|
![godot-smoother-child-of-root-node](https://user-images.githubusercontent.com/7110246/209628202-6339f715-21fc-4529-b42d-a778f871a532.png)
|
||
|
|
||
|
[^1]: Currently `RigidBody2D` and `RigidBody3D` are not supported.
|
||
|
[^2]: Nodes that have no custom `_physics_process` are automatically ignored. So are target properties that a node may not have.
|
||
|
|
||
|
#### Properties
|
||
|
|
||
|
![godot-smoother-default-options](https://user-images.githubusercontent.com/7110246/209629766-595b34f9-309a-453c-bf52-440b2f4940de.png)
|
||
|
|
||
|
*Smoother* default options are:
|
||
|
- **properties**:`Array[String]` = `["position"]`[^3] — The listed properties are interpolated[^4] unless a node does not have the property in which case it will be ignored for that particular node.
|
||
|
- **smooth_parent**:`bool` = `true` — Include the parent node for interpolation.
|
||
|
- **recursive**:`bool` = `true` — Include recursive children. Note that recursive is **relative to the *Smoother*'s parent**. In a way the *Smoother* node attaches to a parent and takes the parent as the base for its operations.[^5]
|
||
|
- **includes**:`Array[NodePath]` = `[]` — Any node listed in this array will be smoothed unless listed in `excludes`.
|
||
|
- **excludes**:`Array[NodePath]` = `[]` — Any node listed in this array will be excluded from smoothing. This overwrites any options above.
|
||
|
|
||
|
[^3]: Note that at the moment of writing, Godot does not display the default `["position"]` value for properties in the inspector, even though the value applies. This may be fixed in a future Godot release.
|
||
|
[^4]: Interpolation only works properties of data types that are supported by [lerp](https://docs.godotengine.org/en/latest/classes/class_@globalscope.html#class-globalscope-method-lerp), i.e. `int`, `float`, `Vector2`, `Vector3`, `Vector4`, `Color`, `Quaternion`, `Basis`
|
||
|
[^5]: The *Smoother* node cannot access nodes above its parent node, it can only act on its parent, parent's children or parent's nested children, except a node higher in the tree hierarchy is an item in `includes`.
|
||
|
|
||
|
Adding other properties that the *Smoother* will attempt to interpolate is as easy as adding the property name strings.
|
||
|
|
||
|
![godot-smoother-options-properties](https://user-images.githubusercontent.com/7110246/209642811-3b268660-c9d9-4679-9e8b-ec1ee9c0a6b9.png)
|
||
|
|
||
|
#### Performance Optimisations
|
||
|
|
||
|
For large levels you may want to optimise things (as you probably should regardless of using the Smoother node). A good approach would be to use the `VisibleOnScreenNotifier2D`/`VisibleOnScreenNotifier3D` and use their `screen_entered` and `screen_exited` signals to update the `includes` or `excludes` array.
|
||
|
|
||
|
##### Method 1: excludes
|
||
|
|
||
|
Add all *off-screen* moveable nodes to `excludes` and remove them when they come *on-screen*, e.g.
|
||
|
``` gdscript
|
||
|
func _on_node_screen_entered(node:Node) -> void:
|
||
|
$Smoother.add_exclude_node(node)
|
||
|
|
||
|
func _on_node_screen_exited(node:Node) -> void:
|
||
|
$Smoother.add_exclude_node(node)
|
||
|
```
|
||
|
Since excludes overwrite all other *Smoother* settings this is the most flexible option.
|
||
|
|
||
|
One caveat is that on entering the tree, the `VisibleOnScreenNotifier2D`/`VisibleOnScreenNotifier3D` do not fire the `screen_exited` signal, so you may have to emit this in a Node's `_enter_tree`, e.g.
|
||
|
``` gdscript
|
||
|
func _enter_tree() -> void:
|
||
|
if !$VisibleOnScreenNotifier2D.is_on_screen():
|
||
|
_on_screen_exited()
|
||
|
```
|
||
|
|
||
|
##### Method 2: includes
|
||
|
|
||
|
Add all *on-screen* moveable nodes to `includes` and remove them when they come *off-screen*, e.g.
|
||
|
``` gdscript
|
||
|
func _on_node_screen_entered(node:Node) -> void:
|
||
|
$Smoother.add_include_node(node)
|
||
|
|
||
|
func _on_node_screen_exited(node:Node) -> void:
|
||
|
$Smoother.remove_include_node(node)
|
||
|
```
|
||
|
Since includes adds nodes but does not interfere with other options you probably should set the `smooth_parent` and `recursive` options to `false`.
|
||
|
|
||
|
On entering the tree, the `VisibleOnScreenNotifier2D`/`VisibleOnScreenNotifier3D` automatically fire the `screen_entered` signal, so nothing needs to be done.
|
||
|
|
||
|
##### For Both Methods
|
||
|
|
||
|
Either way it's probably a good idea to emit the `screen_exited` signal on `_exit_tree` to cleanup the `inludes` or `excludes` array, e.g.
|
||
|
``` gdscript
|
||
|
func _exit_tree() -> void:
|
||
|
emit_signal("screen_exited", self)
|
||
|
```
|
||
|
|
||
|
The [godot-smoother-node-test-scene](https://github.com/anatolbogun/godot-smoother-node-test-scene) uses performance optimisations in [level2d.gd](https://github.com/anatolbogun/godot-smoother-node-test-scene/blob/main/src/Levels/level2d.gd#L18-L37) and some sprite nodes that emit signals as mentioned above.
|
||
|
|
||
|
##### Debugging
|
||
|
|
||
|
You can always check the currently smoothed nodes to see if your performance optimisation works as intended, e.g.
|
||
|
``` gdscript
|
||
|
print("smoothed nodes: ", $Smoother.smoothed_nodes.map(func (node:Node): return node.name))
|
||
|
```
|
||
|
The above code displays the currently smoothed nodes in the Godot debugger when the `includes` or `excludes` array is updated:
|
||
|
![godot-smoother-debugging-smoothed-nodes](https://user-images.githubusercontent.com/7110246/209639351-97a37452-bbfd-494a-8c7e-da4248776b99.png)
|
||
|
|
||
|
#### Teleporting
|
||
|
|
||
|
When teleporting a node (changing the position) you may want to call `reset_node(node)` or `reset_node_path(path)`, otherwise a teleport may not work as expected, e.g.
|
||
|
``` gdscript
|
||
|
func _on_node_teleport_started(node: Node) -> void:
|
||
|
$Smoother.reset_node(node)
|
||
|
```
|
||
|
|
||
|
### Notes
|
||
|
|
||
|
#### Collision Detection
|
||
|
|
||
|
Collision detection still happens in the `_physics_process`, so if the `physics_ticks_per_second` value in the project settings is too low you may experience seemingly incorrect or punishing collision detection. The default 60 `physics_ticks_per_second` should a good choice. To test this node you may want to temporarily reduce physics ticks to a lower value and toggle this node's process mode on and off. The [godot-smoother-node-test-scene](https://github.com/anatolbogun/godot-smoother-node-test-scene) sample project has only 13 `physics_ticks_per_second` for demonstration purposes (not recommended for a "real" project). As a result collision detection is quite inaccurate.
|
||
|
|
||
|
#### Always the First Child
|
||
|
|
||
|
The code will automatically keep the *Smoother* node as the first child of its parent node because its `_physics_process` and `_process` code *must* run before nodes that are interpolated by it.
|
||
|
|
||
|
#### Process Priority
|
||
|
|
||
|
When `smooth_parent` is enabled the `process_priority` will be kept at a lower value than the parent's, i.e. it will be processed earlier, again because the *Smoother*'s `_physics_process` and `_process` code *must* run before nodes that are interpolated by it.
|
||
|
|
||
|
#### Data Structure
|
||
|
The core of this class is the `_properties` dictionary which holds `_physics_process` origin and target values of the relevant nodes and properties. These values are then interpolated in `_process`.
|
||
|
|
||
|
For easier understanding of the code, the structure is:
|
||
|
``` gdscript
|
||
|
_properties[node][property][0] # origin value of a node's property
|
||
|
_properties[node][property][1] # target value of a node's property
|
||
|
```
|
||
|
So for example:
|
||
|
``` gdscript
|
||
|
_properties
|
||
|
├── Player
|
||
|
│ └── position
|
||
|
│ ├── 0:Vector2 = {x: 0, y: 0} # origin
|
||
|
│ └── 1:Vector2 = {x: 10, y: 20} # target
|
||
|
│ └── rotation
|
||
|
│ ├── 0:float = 0 # origin
|
||
|
│ └── 1:float = 15 # target
|
||
|
├── Enemy
|
||
|
│ └── position
|
||
|
│ ├── 0:Vector2 = {x: 100, y: 0} # origin
|
||
|
│ └── 1:Vector2 = {x: 70, y: 0} # target
|
||
|
│ └── rotation
|
||
|
│ ├── 0:float = 0 # origin
|
||
|
│ └── 1:float = -5 # target
|
||
|
:
|
||
|
etc.
|
||
|
```
|
||
|
|
||
|
### Limitations
|
||
|
|
||
|
#### RigidBody2D / RigidBody3D
|
||
|
|
||
|
Currently this class does not work with `RigidBody2D` or `RigidBody3D` nodes. Please check out https://github.com/lawnjelly/smoothing-addon/ which has a more complicated setup but rewards the effort with more precision and less limitations. Or help to make this code work with rigid bodies if it's possible at all.
|
||
|
|
||
|
#### One Step Behind
|
||
|
|
||
|
Interpolation is one `_physics_process` step behind because we need to know the origin and target values for an interpolation to occur, so in a typical scenario this means a delay of 1/60 second which is the default `physics_ticks_per_second` in the project settings.
|
||
|
|
||
|
#### No Look Ahead
|
||
|
|
||
|
Interpolation does not look ahead for collision detection. That means that for example if a sprite falls to hit the ground and the last `_physics_process` step before impact is very close to the ground, interpolation will still occur on all `_physics` frames between which may have a slight impact cushioning effect. However, with 60 physics fps this is hopefully negligible.
|
||
|
|
||
|
#### Godot 4
|
||
|
|
||
|
This class is written in GDScript 2 for Godot 4+, but feel free to get in touch and we can add a `godot-3` branch or fork the project and make adjustments. It's probably not too hard to backport since it only relies on other nodes' properties, the `position` property by default.
|
||
|
|
||
|
|
||
|
### Support
|
||
|
|
||
|
I'm fairly new to Godot, so if you find any bugs or have suggestions for performance improvements in the *Smoother* code for example, please let me know.
|
||
|
|
||
|
I haven't tested this much yet, primarily only in the 2d and 3d test levels in the [godot-smoother-node-test-scene](https://github.com/anatolbogun/godot-smoother-node-test-scene).
|