From 4d6af3d0981fa761bbadc2b4d66318ecf4dc60e7 Mon Sep 17 00:00:00 2001 From: Spencer Killen Date: Sat, 25 May 2024 10:10:24 -0600 Subject: [PATCH] initial project and basic controller --- .gitattributes | 15 +++ .gitignore | 7 + addons/smoother/LICENSE | 21 +++ addons/smoother/README.md | 184 +++++++++++++++++++++++++ addons/smoother/icon.png | 3 + addons/smoother/icon.png.import | 34 +++++ addons/smoother/smoother.gd | 229 ++++++++++++++++++++++++++++++++ attributions.txt | 1 + blends/.gdignore | 0 blends/level.blend | 3 + blends/player.blend | 3 + blends/shared_export.py | 92 +++++++++++++ camera/camera.gd | 29 ++++ camera/camera.tscn | 15 +++ icon.svg | 1 + icon.svg.import | 37 ++++++ level/level.glb | 3 + level/level.glb.import | 47 +++++++ level/level.tscn | 46 +++++++ level/level_sand.png | 3 + level/level_sand.png.import | 38 ++++++ level/materials/Sand.tres | 10 ++ level/materials/sand.png | 3 + level/materials/sand.png.import | 34 +++++ level/world_environment.tres | 11 ++ player/player.gd | 27 ++++ player/player.glb | 3 + player/player.glb.import | 34 +++++ player/player.tscn | 24 ++++ project.godot | 38 ++++++ 30 files changed, 995 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 addons/smoother/LICENSE create mode 100644 addons/smoother/README.md create mode 100644 addons/smoother/icon.png create mode 100644 addons/smoother/icon.png.import create mode 100644 addons/smoother/smoother.gd create mode 100644 attributions.txt create mode 100644 blends/.gdignore create mode 100644 blends/level.blend create mode 100644 blends/player.blend create mode 100644 blends/shared_export.py create mode 100644 camera/camera.gd create mode 100644 camera/camera.tscn create mode 100644 icon.svg create mode 100644 icon.svg.import create mode 100644 level/level.glb create mode 100644 level/level.glb.import create mode 100644 level/level.tscn create mode 100644 level/level_sand.png create mode 100644 level/level_sand.png.import create mode 100644 level/materials/Sand.tres create mode 100644 level/materials/sand.png create mode 100644 level/materials/sand.png.import create mode 100644 level/world_environment.tres create mode 100644 player/player.gd create mode 100644 player/player.glb create mode 100644 player/player.glb.import create mode 100644 player/player.tscn create mode 100644 project.godot diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f7c3e11 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +* text=auto eol=lf +*.glb filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.material filter=lfs diff=lfs merge=lfs -text +*.mdd filter=lfs diff=lfs merge=lfs -text +*.abc filter=lfs diff=lfs merge=lfs -text +*.ogg filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.blend filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.pur filter=lfs diff=lfs merge=lfs -text +*.res filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f5dbcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Godot 4+ specific ignores +.godot/ + +# Blender Ignores +*.blend1 +__pycache__/**/* +*.pyc diff --git a/addons/smoother/LICENSE b/addons/smoother/LICENSE new file mode 100644 index 0000000..726d27a --- /dev/null +++ b/addons/smoother/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Anatol Bogun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addons/smoother/README.md b/addons/smoother/README.md new file mode 100644 index 0000000..75316f6 --- /dev/null +++ b/addons/smoother/README.md @@ -0,0 +1,184 @@ +# 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). diff --git a/addons/smoother/icon.png b/addons/smoother/icon.png new file mode 100644 index 0000000..659180a --- /dev/null +++ b/addons/smoother/icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f71a2d795b56e9c585b3f7d2ea5e706e09bf3ee6519aabd8705098512d582f7 +size 17156 diff --git a/addons/smoother/icon.png.import b/addons/smoother/icon.png.import new file mode 100644 index 0000000..335cf29 --- /dev/null +++ b/addons/smoother/icon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dyuji2reawgld" +path="res://.godot/imported/icon.png-036e47a3038781269e79b77dd42621d5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/smoother/icon.png" +dest_files=["res://.godot/imported/icon.png-036e47a3038781269e79b77dd42621d5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/smoother/smoother.gd b/addons/smoother/smoother.gd new file mode 100644 index 0000000..b1b2fd5 --- /dev/null +++ b/addons/smoother/smoother.gd @@ -0,0 +1,229 @@ +# MIT LICENSE +# +# Copyright 2022 Anatol Bogun +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +# associated documentation files (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, publish, distribute, +# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all copies or +# substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +# NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +class_name Smoother extends Node + +## Smoother Node +## Version: 1.0.4 +## +## A node type that smoothes scene nodes' properties by interpolating _physics_process steps. +## +## For documentation please visit https://github.com/anatolbogun/godot-smoother-node . + +## Node properties that are interpolated. +## Defaults to ["position"], even if not displayed in the inspector. +@export var properties:Array[String] = ["position"] + +## Apply interpolation to this node's parent. +@export var smooth_parent: = true : + set (value): + if value == false: + # remove parent from _properties in case this gets toggled on and off during runtime + _properties.erase(get_parent()) + + smooth_parent = value + +## Apply interpolation to the recursive children of this node's parent. +@export var recursive: = true + +## Explicitly include node paths in addition to the nodes that are included by other Smoother +## settings. +@export var includes:Array[NodePath] = [] + +## Explicitly exclude node paths. +## This will exclude nodes that would otherwise be included by other settings. +@export var excludes:Array[NodePath] = [] + +# get an array of all currently smoothed nodes; mainly for debugging performance optimisations +var smoothed_nodes:Array[Node] : + get: + var parent: = get_parent() + return _get_physics_process_nodes(parent, !smooth_parent) if parent != null else [] as Array[Node] + +var _properties: = {} +var _physics_process_nodes:Array[Node] +var _physics_process_just_updated: = false + + +## Reset all smoothed nodes. +func reset() -> void: + _properties.clear() + + +## Reset a specific node. You may want to call this when a node gets teleported. +func reset_node(node:Node) -> void: + _properties.erase(node) + + +## Reset a specific Node by NodePath. You may want to call this when a Node gets teleported. +func reset_node_path(path:NodePath) -> void: + var node: = get_node_or_null(path) + + if node != null: + reset_node(node) + + +## Add a Node to the includes Array[NodePath]. +func add_include_node(node:Node) -> Array[NodePath]: + return add_include_path(get_path_to(node)) + + +## Add a NodePath to the includes Array[NodePath]. +func add_include_path(path:NodePath) -> Array[NodePath]: + return _add_unique_to_array(includes, path) as Array[NodePath] + + +## Remove a Node from the includes Array[NodePath]. +func remove_include_node(node:Node) -> Array[NodePath]: + return remove_include_path(get_path_to(node)) + + +## Remove a NodePath from the includes Array[NodePath]. +func remove_include_path(path:NodePath) -> Array[NodePath]: + return _remove_all_from_array(includes, path) as Array[NodePath] + + +## Add a Node to the excludes Array[NodePath]. +func add_exclude_node(node:Node) -> Array[NodePath]: + return add_exclude_path(get_path_to(node)) + + +## Add a NodePath to the excludes Array[NodePath]. +func add_exclude_path(path:NodePath) -> Array[NodePath]: + return _add_unique_to_array(excludes, path) as Array[NodePath] + + +## Remove a Node from the excludes Array[NodePath]. +func remove_exclude_node(node:Node) -> Array[NodePath]: + return remove_exclude_path(get_path_to(node)) + + +## Remove a NodePath from the excludes Array[NodePath]. +func remove_exclude_path(path:NodePath) -> Array[NodePath]: + return _remove_all_from_array(excludes, path) as Array[NodePath] + + +## Add an item to an array unless the array already contains that item. +func _add_unique_to_array(array:Array, item:Variant) -> Array: + if !array.has(item): + array.push_back(item) + + return array + + +## Remove all array items that match item. +func _remove_all_from_array(array:Array, item:Variant) -> Array: + while array.has(item): + array.erase(item) + + return array + + +## Apply interpolation to all smoothed_nodes supported properties. +func _process(_delta: float) -> void: + for node in _physics_process_nodes: + if !_properties.has(node): continue + + for property in _properties[node]: + var values = _properties[node][property] + + if values.size() == 2: + if _physics_process_just_updated: + values[1] = node[property] + + node[property] = lerp(values[0], values[1], Engine.get_physics_interpolation_fraction()) + + _physics_process_just_updated = false + + +## Store all smoothed_nodes' relevant properties of the previous (origin) and this (target) +## _physics_process frames for interpolation in the upcoming _process frames and apply the origin +## values. +func _physics_process(_delta: float) -> void: + var parent: = get_parent() + if parent == null: return + + # move this node to the top of the parent tree (typically a scene's root node) so that it is + # called before all other _physics_processes + parent.move_child(self, 0) + + if smooth_parent: + process_priority = parent.process_priority - 1 + + # update the relevant nodes once per _physics_process + _physics_process_nodes = _get_physics_process_nodes(parent, !smooth_parent) + + # clean up _properties + for key in _properties.keys(): + if !_physics_process_nodes.has(key): + _properties.erase(key) + + for node in _physics_process_nodes: + if !_properties.has(node): + # called on the first frame after a node was added to _properties + _properties[node] = {} + + # clean up _properties when a node exited the tree + node.tree_exited.connect(func (): _properties.erase(node)) + + for property in properties: + if ! property in node: continue + + if !_properties[node].has(property): + # called on the first frame after a node was added to _properties + _properties[node][property] = [node[property]] + elif _properties[node][property].size() < 2: + # called on the second frame after a node was added to _properties + _properties[node][property].push_front(_properties[node][property][0]) + _properties[node][property][1] = node[property] + else: + _properties[node][property][0] = _properties[node][property][1] + node[property] = _properties[node][property][0] + + _physics_process_just_updated = true + + +## Get the relevant nodes to be smoothed based on this node's tree position and properties. +func _get_physics_process_nodes(node: Node, ignore_node: = false, with_includes: = true) -> Array[Node]: + var nodes:Array[Node] = [] + + nodes.assign(includes.map( + get_node_or_null + ).filter( + func (_node:Node) -> bool: return _node != null && !excludes.has(get_path_to(_node)) + ) if with_includes else []) + + if ( + !ignore_node + && node != self + && !node is RigidBody2D + && !node is RigidBody3D + && !nodes.has(node) + && !excludes.has(get_path_to(node)) + && node.has_method("_physics_process") + ): + nodes.push_back(node) + + if recursive: + for child in node.get_children(): + for nested_node in _get_physics_process_nodes(child, false, false): + _add_unique_to_array(nodes, nested_node) + + return nodes diff --git a/attributions.txt b/attributions.txt new file mode 100644 index 0000000..a0e7d09 --- /dev/null +++ b/attributions.txt @@ -0,0 +1 @@ +https://www.hippopng.com/png-btykal/ diff --git a/blends/.gdignore b/blends/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/blends/level.blend b/blends/level.blend new file mode 100644 index 0000000..39b143f --- /dev/null +++ b/blends/level.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:964d656fad5cf7932f03869bb66a1f63aeaa510dc0460b6aff5af944a0440b1e +size 946380 diff --git a/blends/player.blend b/blends/player.blend new file mode 100644 index 0000000..4046bf4 --- /dev/null +++ b/blends/player.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5c0204a39954216753ac70efe5fd21ba89178a9b506d3b9006816e11d5cc357 +size 961776 diff --git a/blends/shared_export.py b/blends/shared_export.py new file mode 100644 index 0000000..3cf14ab --- /dev/null +++ b/blends/shared_export.py @@ -0,0 +1,92 @@ +""" +Common functions useful for exporting blender scenes to GLTF with bpy +""" + + +from pathlib import Path +from sys import stderr +import bpy +from bpy.types import Object, Armature, Modifier, Mesh, Context, Collection +from functools import reduce +from operator import attrgetter, itemgetter, or_ + + + +def clear_selection(ctx: Context): + for obj in ctx.selected_objects: + obj.select_set(False) + + +def select_collection(ctx: Context, c: Collection): + for obj in c.objects: + obj.select_set(True) + +# https://blenderartists.org/t/how-to-apply-all-the-modifiers-with-python/1314483/2 + + +def apply_modifier(obj: Object, modifier: Modifier): + with bpy.context.temp_override(object=obj, modifier=modifier): + bpy.context.view_layer.update() + bpy.ops.object.modifier_apply(modifier=modifier.name) + bpy.context.view_layer.update() + + +def sort_uv_layers(mesh: Mesh): + # UV layers use non-pythonic reference semantics, safest to always reference by their indices + assert len( + mesh.uv_layers) <= 7, "There must be fewer than 8 UV maps to sort (one less than the Blender maximum of 8)" + initial_layers = [[i, layer.name] + for i, layer in enumerate(mesh.uv_layers)] + initial_layers.sort(key=itemgetter(1)) + for i, layer_name in tuple(initial_layers): + assert mesh.uv_layers[i].name == layer_name, mesh.uv_layers[i].name + \ + " " + layer_name + mesh.uv_layers.active = mesh.uv_layers[i] + new_layer = mesh.uv_layers.new(name=f"new_{mesh.uv_layers[i].name}") + assert new_layer == mesh.uv_layers[-1] + mesh.uv_layers.active = mesh.uv_layers[0] + mesh.uv_layers.remove(mesh.uv_layers[i]) + for pair in initial_layers: + if pair[0] > i: + pair[0] -= 1 + for layer in mesh.uv_layers: + layer.name = layer.name.replace("new_", "") + mesh.uv_layers.active = mesh.uv_layers[0] + +def delete_uv_layer(mesh: Mesh, name: str): + for layer in mesh.uv_layers: + if layer.name == name: + mesh.uv_layers.remove(layer) + return + raise Exception(f"failed to remove UV layer with name {name}") + +def convert_attribute(obj, attribute_name: str): + "Don't convert this into a function that does multiple at once, indices are too fucky for that" + for i, attr in enumerate(obj.data.attributes): + if attr.name == attribute_name: + obj.data.attributes.active_index = i + bpy.context.view_layer.update() + bpy.ops.geometry.attribute_convert( + mode='VERTEX_GROUP', domain='POINT', data_type='FLOAT') + bpy.context.view_layer.update() + return + raise Exception("attribute not found", attribute_name) + + +def apply_bone_groups(rig: Armature, skinned: Object): + """May not be needed in future versions of Blender + Converts geometry nodes attributes for bone groups back to vertex groups + Something fishy with indices and attributes, so algorithm is not efficient just to be safe + """ + with bpy.context.temp_override(object=skinned): + for bone_name in map(attrgetter("name"), rig.bones): + for i, attribute in enumerate(skinned.data.attributes): + if attribute.name == bone_name: + skinned.data.attributes.active_index = i + bpy.context.view_layer.update() + bpy.ops.geometry.attribute_convert( + mode='VERTEX_GROUP', domain='POINT', data_type='FLOAT') + bpy.context.view_layer.update() + break + else: + print("Failed to find attribute for", bone_name, file=stderr) diff --git a/camera/camera.gd b/camera/camera.gd new file mode 100644 index 0000000..71cf86c --- /dev/null +++ b/camera/camera.gd @@ -0,0 +1,29 @@ +extends Node3D + +const MOUSE_SENSITIVITY := .001 + +func get_input_direction() -> Vector2: + var input := Input.get_vector("move_left", "move_right", "move_up", "move_down") + if input == Vector2.ZERO: + return Vector2.ZERO + var dir := input.normalized() + + input = input * dir.abs() + return -1.0 * dir.rotated(-rotation.y + PI) + +func _process(_delta): + get_parent().movement_dir = get_input_direction() + %SpringArm3D.look_at(global_position) + +func _ready(): + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + +func handle_camera_movement(move: Vector2): + if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED: + return + move *= MOUSE_SENSITIVITY + rotate_y(-move.x) + +func _input(event): + if event is InputEventMouseMotion: + handle_camera_movement(event.relative) diff --git a/camera/camera.tscn b/camera/camera.tscn new file mode 100644 index 0000000..efabb94 --- /dev/null +++ b/camera/camera.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=2 format=3 uid="uid://brgqf2ebuyhuy"] + +[ext_resource type="Script" path="res://camera/camera.gd" id="1_veqr4"] + +[node name="Camera" type="Node3D"] +script = ExtResource("1_veqr4") + +[node name="SpringArm3D" type="SpringArm3D" parent="."] +unique_name_in_owner = true +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 2) +spring_length = 2.0 +margin = 0.5 + +[node name="Camera3D" type="Camera3D" parent="SpringArm3D"] +unique_name_in_owner = true diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..3fe4f4a --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..9ef05e8 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cddalye5wfvl6" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/level/level.glb b/level/level.glb new file mode 100644 index 0000000..f068d26 --- /dev/null +++ b/level/level.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a316d004b440e30b0683afc0d23bb6ad6fa5b2e7cec87494d954d0e34f52b9c9 +size 366820 diff --git a/level/level.glb.import b/level/level.glb.import new file mode 100644 index 0000000..3289439 --- /dev/null +++ b/level/level.glb.import @@ -0,0 +1,47 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://bq654gwim6col" +path="res://.godot/imported/level.glb-f09dab4240afab499892fe27d8f400a9.scn" + +[deps] + +source_file="res://level/level.glb" +dest_files=["res://.godot/imported/level.glb-f09dab4240afab499892fe27d8f400a9.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/apply_root_scale=true +nodes/root_scale=1.0 +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +import_script/path="" +_subresources={ +"materials": { +"Sand": { +"use_external/enabled": true, +"use_external/path": "res://level/materials/Sand.tres" +} +}, +"nodes": { +"PATH:Cube": { +"generate/physics": true, +"physics/shape_type": 2 +} +} +} +gltf/naming_version=1 +gltf/embedded_image_handling=1 diff --git a/level/level.tscn b/level/level.tscn new file mode 100644 index 0000000..ce74d25 --- /dev/null +++ b/level/level.tscn @@ -0,0 +1,46 @@ +[gd_scene load_steps=8 format=3 uid="uid://b00brfkibo5cj"] + +[ext_resource type="PackedScene" uid="uid://bq654gwim6col" path="res://level/level.glb" id="1_s37in"] +[ext_resource type="PackedScene" uid="uid://do25xvpy80iio" path="res://player/player.tscn" id="2_7ct70"] +[ext_resource type="Environment" uid="uid://covjrwmk4rplw" path="res://level/world_environment.tres" id="2_ptkl6"] +[ext_resource type="PackedScene" uid="uid://brgqf2ebuyhuy" path="res://camera/camera.tscn" id="3_yev7j"] +[ext_resource type="Script" path="res://addons/smoother/smoother.gd" id="5_2tyle"] + +[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_ujmev"] +margin = 2.067 + +[sub_resource type="BoxShape3D" id="BoxShape3D_qp06x"] +size = Vector3(2, 0.1, 2) + +[node name="level" instance=ExtResource("1_s37in")] + +[node name="WorldEnvironment" type="WorldEnvironment" parent="." index="0"] +environment = ExtResource("2_ptkl6") + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="WorldEnvironment" index="0"] +transform = Transform3D(1, 0, 0, 0, 0.566018, 0.824393, 0, -0.824393, 0.566018, 0, 13.4573, 0) + +[node name="StaticBody3D" type="StaticBody3D" parent="Plane" index="0"] + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Plane/StaticBody3D" index="0"] +transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0) +shape = SubResource("WorldBoundaryShape3D_ujmev") + +[node name="CollisionShape3D2" type="CollisionShape3D" parent="Plane/StaticBody3D" index="1"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0) +shape = SubResource("BoxShape3D_qp06x") + +[node name="Player" parent="." index="3" instance=ExtResource("2_7ct70")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.36667, 2.66437, 0) +floor_max_angle = 1.309 +JUMP_VELOCITY = 5.0 + +[node name="Camera" parent="Player" index="2" instance=ExtResource("3_yev7j")] + +[node name="Players" type="Node3D" parent="." index="4"] + +[node name="Smoother" type="Node" parent="." index="5"] +script = ExtResource("5_2tyle") + +[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="." index="6"] +spawn_path = NodePath("../Players") diff --git a/level/level_sand.png b/level/level_sand.png new file mode 100644 index 0000000..e1905c6 --- /dev/null +++ b/level/level_sand.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3733797f4f86e09292632315d94ac860412981d6ad3e132f93845de296b0b5d +size 358428 diff --git a/level/level_sand.png.import b/level/level_sand.png.import new file mode 100644 index 0000000..12246e7 --- /dev/null +++ b/level/level_sand.png.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://babdfqbjlgnn" +path.s3tc="res://.godot/imported/level_sand.png-c6e8f007b14b98f18a4346ab72e8719d.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} +generator_parameters={ +"md5": "dd9f28b10e384aa5f76555a302d90a36" +} + +[deps] + +source_file="res://level/level_sand.png" +dest_files=["res://.godot/imported/level_sand.png-c6e8f007b14b98f18a4346ab72e8719d.s3tc.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=true +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/level/materials/Sand.tres b/level/materials/Sand.tres new file mode 100644 index 0000000..a91fd2f --- /dev/null +++ b/level/materials/Sand.tres @@ -0,0 +1,10 @@ +[gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://b3nngj6l17khj"] + +[ext_resource type="Texture2D" uid="uid://babdfqbjlgnn" path="res://level/level_sand.png" id="1_p08dh"] + +[resource] +resource_name = "Sand" +cull_mode = 2 +albedo_texture = ExtResource("1_p08dh") +roughness = 0.5 +uv1_scale = Vector3(3.67, 3.67, 3.67) diff --git a/level/materials/sand.png b/level/materials/sand.png new file mode 100644 index 0000000..e1905c6 --- /dev/null +++ b/level/materials/sand.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3733797f4f86e09292632315d94ac860412981d6ad3e132f93845de296b0b5d +size 358428 diff --git a/level/materials/sand.png.import b/level/materials/sand.png.import new file mode 100644 index 0000000..ae00235 --- /dev/null +++ b/level/materials/sand.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://445svcjk41fn" +path="res://.godot/imported/sand.png-20c37fc6a1ebebf936f842368a0c94a6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://level/materials/sand.png" +dest_files=["res://.godot/imported/sand.png-20c37fc6a1ebebf936f842368a0c94a6.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/level/world_environment.tres b/level/world_environment.tres new file mode 100644 index 0000000..6ecd9c2 --- /dev/null +++ b/level/world_environment.tres @@ -0,0 +1,11 @@ +[gd_resource type="Environment" load_steps=3 format=3 uid="uid://covjrwmk4rplw"] + +[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_5o4sp"] + +[sub_resource type="Sky" id="Sky_nb6lr"] +sky_material = SubResource("ProceduralSkyMaterial_5o4sp") + +[resource] +background_mode = 2 +sky = SubResource("Sky_nb6lr") +ambient_light_source = 3 diff --git a/player/player.gd b/player/player.gd new file mode 100644 index 0000000..fb03c80 --- /dev/null +++ b/player/player.gd @@ -0,0 +1,27 @@ +# Automatically Generated From Builtin CharacterBody Template + +extends CharacterBody3D + +@export var SPEED = 5.0 +@export var JUMP_VELOCITY = 4.5 + +var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") + +# Set by camera child, switch to signals if becomes too spaghetti +var movement_dir: Vector2 = Vector2.ZERO + +func _physics_process(delta): + if not is_on_floor(): + velocity.y -= gravity * delta + + if Input.is_action_just_pressed("ui_accept") and is_on_floor(): + velocity.y = JUMP_VELOCITY + + if movement_dir: + velocity.x = movement_dir.x * SPEED + velocity.z = movement_dir.y * SPEED + else: + velocity.x = move_toward(velocity.x, 0, SPEED) + velocity.z = move_toward(velocity.z, 0, SPEED) + + move_and_slide() diff --git a/player/player.glb b/player/player.glb new file mode 100644 index 0000000..cac6fa6 --- /dev/null +++ b/player/player.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a3e75b49d45922e4578447724c1c5fe8163e6a35fe95e142ae008d792ba2e8f +size 6780 diff --git a/player/player.glb.import b/player/player.glb.import new file mode 100644 index 0000000..f8ce2b5 --- /dev/null +++ b/player/player.glb.import @@ -0,0 +1,34 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://bwg2jkbq7gada" +path="res://.godot/imported/player.glb-d5e59c3624fa2635da7ea043f9526ccc.scn" + +[deps] + +source_file="res://player/player.glb" +dest_files=["res://.godot/imported/player.glb-d5e59c3624fa2635da7ea043f9526ccc.scn"] + +[params] + +nodes/root_type="CharacterBody3D" +nodes/root_name="" +nodes/apply_root_scale=true +nodes/root_scale=1.0 +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +import_script/path="" +_subresources={} +gltf/naming_version=1 +gltf/embedded_image_handling=1 diff --git a/player/player.tscn b/player/player.tscn new file mode 100644 index 0000000..16af845 --- /dev/null +++ b/player/player.tscn @@ -0,0 +1,24 @@ +[gd_scene load_steps=5 format=3 uid="uid://do25xvpy80iio"] + +[ext_resource type="PackedScene" uid="uid://bwg2jkbq7gada" path="res://player/player.glb" id="1_0u2un"] +[ext_resource type="Script" path="res://player/player.gd" id="1_gh340"] + +[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_u4bmc"] +properties/0/path = NodePath(".:position") +properties/0/spawn = true +properties/0/replication_mode = 1 +properties/1/path = NodePath("player:velocity") +properties/1/spawn = true +properties/1/replication_mode = 1 + +[sub_resource type="SphereShape3D" id="SphereShape3D_t1htn"] +radius = 0.986757 + +[node name="player" instance=ExtResource("1_0u2un")] +script = ExtResource("1_gh340") + +[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." index="0"] +replication_config = SubResource("SceneReplicationConfig_u4bmc") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="." index="2"] +shape = SubResource("SphereShape3D_t1htn") diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..bd7a605 --- /dev/null +++ b/project.godot @@ -0,0 +1,38 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Grounders" +config/features=PackedStringArray("4.2", "Forward Plus") +config/icon="res://icon.svg" + +[input] + +move_left={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"echo":false,"script":null) +] +} +move_up={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"echo":false,"script":null) +] +} +move_right={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"echo":false,"script":null) +] +} +move_down={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"echo":false,"script":null) +] +}