initial project and basic controller

This commit is contained in:
Spencer Killen 2024-05-25 10:10:24 -06:00
commit 4d6af3d098
Signed by: sjkillen
GPG Key ID: 3AF3117BA6FBB75B
30 changed files with 995 additions and 0 deletions

15
.gitattributes vendored Normal file
View File

@ -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

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# Godot 4+ specific ignores
.godot/
# Blender Ignores
*.blend1
__pycache__/**/*
*.pyc

21
addons/smoother/LICENSE Normal file
View File

@ -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.

184
addons/smoother/README.md Normal file
View File

@ -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).

BIN
addons/smoother/icon.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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

229
addons/smoother/smoother.gd Normal file
View File

@ -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

1
attributions.txt Normal file
View File

@ -0,0 +1 @@
https://www.hippopng.com/png-btykal/

0
blends/.gdignore Normal file
View File

BIN
blends/level.blend (Stored with Git LFS) Normal file

Binary file not shown.

BIN
blends/player.blend (Stored with Git LFS) Normal file

Binary file not shown.

92
blends/shared_export.py Normal file
View File

@ -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)

29
camera/camera.gd Normal file
View File

@ -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)

15
camera/camera.tscn Normal file
View File

@ -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

1
icon.svg Normal file
View File

@ -0,0 +1 @@
<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="124" height="124" rx="14" fill="#363d52" stroke="#212532" stroke-width="4"/><g transform="scale(.101) translate(122 122)"><g fill="#fff"><path d="M105 673v33q407 354 814 0v-33z"/><path d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z" fill="#478cbf"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></g></svg>

After

Width:  |  Height:  |  Size: 949 B

37
icon.svg.import Normal file
View File

@ -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

BIN
level/level.glb (Stored with Git LFS) Normal file

Binary file not shown.

47
level/level.glb.import Normal file
View File

@ -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

46
level/level.tscn Normal file
View File

@ -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")

BIN
level/level_sand.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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

10
level/materials/Sand.tres Normal file
View File

@ -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)

BIN
level/materials/sand.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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

View File

@ -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

27
player/player.gd Normal file
View File

@ -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()

BIN
player/player.glb (Stored with Git LFS) Normal file

Binary file not shown.

34
player/player.glb.import Normal file
View File

@ -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

24
player/player.tscn Normal file
View File

@ -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")

38
project.godot Normal file
View File

@ -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)
]
}