commit b656d6e772b93a2782ce1f13bbc54d500783002e Author: copygirl Date: Sun Apr 16 10:47:18 2023 +0200 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed3db4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Godot 4+ specific ignore. +.godot/ + +# Don't include git addon. +addons/godot-git-plugin/ + +# Ignore blender backups. +*.blend[0-9] diff --git a/Player.cs b/Player.cs new file mode 100644 index 0000000..a2ce0dd --- /dev/null +++ b/Player.cs @@ -0,0 +1,158 @@ +using Godot; +using System; + +public partial class Player : CharacterBody3D +{ + public float MouseSensitivity { get; set; } = 0.2F; + + /// Time after pressing the jump button a jump may occur late. + public TimeSpan JumpEarlyTime { get; set; } = TimeSpan.FromSeconds(0.2); + + /// Time after leaving a jumpable surface when a jump may still occur. + public TimeSpan JumpCoyoteTime { get; set; } = TimeSpan.FromSeconds(0.2); + + public Vector3 Gravity { get; set; } = new(0, -12.0F, 0); + public float JumpVelocity { get; set; } = 5.0F; + public float MoveAccel { get; set; } = 6.0F; + public float MaxMoveSpeed { get; set; } = 4.0F; + public float FrictionFloor { get; set; } = 12.0F; + public float FrictionAir { get; set; } = 2.0F; + + public enum MovementMode { Default, Flying, NoClip } + public MovementMode Movement { get; set; } = MovementMode.Default; + + + private Node3D _neckBone = null!; + private Node3D _headBone = null!; + private Camera3D _camera = null!; + + private DateTime? _jumpPressed = null; + private DateTime? _lastOnFloor = null; + + public bool IsSprinting { get; private set; } + + + public override void _Ready() + { + _neckBone = GetNode("Neck"); + _headBone = GetNode("Neck/Head"); + _camera = GetNode("Neck/Head/Camera"); + } + + public override void _Input(InputEvent ev) + { + // Inputs that are valid when the game is focused. + // =============================================== + + if (ev.IsAction("move_sprint")) + { + IsSprinting = ev.IsPressed(); + GetViewport().SetInputAsHandled(); + } + + if (ev.IsActionPressed("move_jump")) + { + _jumpPressed = DateTime.Now; + GetViewport().SetInputAsHandled(); + } + + // Cycle movement mode between default, flying and flying+noclip. + if (ev.IsActionPressed("cycle_movement_mode")) + { + if (++Movement > MovementMode.NoClip) + Movement = MovementMode.Default; + GetViewport().SetInputAsHandled(); + } + + // Inputs that are valid only when the mouse is captured. + // ====================================================== + if (Input.MouseMode == Input.MouseModeEnum.Captured) { + } + } + + public override void _UnhandledInput(InputEvent ev) + { + var isMouseCaptured = Input.MouseMode == Input.MouseModeEnum.Captured; + // When pressing escape and mouse is currently captured, release it. + if (ev.IsActionPressed("ui_cancel") && isMouseCaptured) + Input.MouseMode = Input.MouseModeEnum.Visible; + + // Grab the mouse when pressing the primary mouse button. + // TODO: Make "primary mouse button" configurable. + if (ev is InputEventMouseButton button && button.ButtonIndex == MouseButton.Left) + Input.MouseMode = Input.MouseModeEnum.Captured; + + if (ev is InputEventMouseMotion motion && isMouseCaptured) + { + _neckBone.RotateX(Mathf.DegToRad(motion.Relative.Y * -MouseSensitivity)); + _headBone.RotateY(Mathf.DegToRad(motion.Relative.X * -MouseSensitivity)); + + var rotation = _neckBone.RotationDegrees; + rotation.X = Mathf.Clamp(rotation.X, -80, 80); + _neckBone.RotationDegrees = rotation; + } + } + + public override void _PhysicsProcess(double delta) + { + var movementVector = new Vector3( + Input.GetActionStrength("move_strafe_right") - Input.GetActionStrength("move_strafe_left"), + Input.GetActionStrength("move_upward") - Input.GetActionStrength("move_downward"), + Input.GetActionStrength("move_backward") - Input.GetActionStrength("move_forward")); + + if (Movement == MovementMode.Default) + { + Velocity += Gravity * (float)delta; + + var dir = Vector3.Zero; + var camTransform = _camera.GlobalTransform; + dir += camTransform.Basis.Z.Normalized() * movementVector.Z; + dir += camTransform.Basis.X.Normalized() * movementVector.X; + dir.Y = 0; + dir = dir.Normalized() * movementVector.Length(); + + var hvel = Velocity; + hvel.Y = 0; + + var target = dir * MaxMoveSpeed; + var friction = IsOnFloor() ? FrictionFloor : FrictionAir; + var accel = (dir.Dot(hvel) > 0) ? MoveAccel : friction; + + if (IsSprinting) { target *= 5; accel *= 5; } + hvel = hvel.Lerp(target, accel * (float)delta); + + Velocity = new(hvel.X, Velocity.Y, hvel.Z); + + // Sometimes, when pushing into a wall, jumping wasn't working. + // Possibly due to `IsOnFloor` returning `false` for some reason. + // The `JumpEarlyTime` feature seems to avoid this issue, thankfully. + + if (IsOnFloor()) _lastOnFloor = DateTime.Now; + + if (((DateTime.Now - _jumpPressed) <= JumpEarlyTime) + && ((DateTime.Now - _lastOnFloor) <= JumpCoyoteTime)) + { + Velocity = new(Velocity.X, JumpVelocity, Velocity.Z); + _jumpPressed = null; + _lastOnFloor = null; + } + } + else + { + Velocity *= 1 - FrictionAir * (float)delta; + + var cameraRot = _headBone.GlobalTransform.Basis.GetRotationQuaternion(); + var dir = cameraRot * movementVector; + var target = dir * MaxMoveSpeed; + var accel = (dir.Dot(Velocity) > 0) ? MoveAccel : FrictionAir; + target *= 4; accel *= 4; + + if (IsSprinting) { target *= 5; accel *= 5; } + Velocity = Velocity.Lerp(target, accel * (float)delta); + } + + if (Movement == MovementMode.NoClip) + Translate(Velocity * (float)delta); + else MoveAndSlide(); + } +} diff --git a/assets/character.blend b/assets/character.blend new file mode 100644 index 0000000..1999d48 Binary files /dev/null and b/assets/character.blend differ diff --git a/assets/character.glb b/assets/character.glb new file mode 100644 index 0000000..1ced075 Binary files /dev/null and b/assets/character.glb differ diff --git a/assets/character.glb.import b/assets/character.glb.import new file mode 100644 index 0000000..dea2c6b --- /dev/null +++ b/assets/character.glb.import @@ -0,0 +1,32 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://brbatwk2mtko4" +path="res://.godot/imported/character.glb-9c9a0ab135b4ff50eb19bfdefe1081c3.scn" + +[deps] + +source_file="res://assets/character.glb" +dest_files=["res://.godot/imported/character.glb-9c9a0ab135b4ff50eb19bfdefe1081c3.scn"] + +[params] + +nodes/root_type="Node3D" +nodes/root_name="Scene Root" +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 +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +import_script/path="" +_subresources={} +gltf/embedded_image_handling=1 diff --git a/assets/terrain_grass.png b/assets/terrain_grass.png new file mode 100644 index 0000000..d31f6c5 Binary files /dev/null and b/assets/terrain_grass.png differ diff --git a/assets/terrain_grass.png.import b/assets/terrain_grass.png.import new file mode 100644 index 0000000..e294789 --- /dev/null +++ b/assets/terrain_grass.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bmormqcp2qv4k" +path.s3tc="res://.godot/imported/terrain_grass.png-b5e6be896538434c286abbba16efbfcb.s3tc.ctex" +metadata={ +"imported_formats": ["s3tc_bptc"], +"vram_texture": true +} + +[deps] + +source_file="res://assets/terrain_grass.png" +dest_files=["res://.godot/imported/terrain_grass.png-b5e6be896538434c286abbba16efbfcb.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/icon.svg b/icon.svg new file mode 100644 index 0000000..adc26df --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..395a6f3 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bmc3w2hmvgi2q" +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/project.godot b/project.godot new file mode 100644 index 0000000..d93c5a5 --- /dev/null +++ b/project.godot @@ -0,0 +1,120 @@ +; 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="Inventory" +run/main_scene="res://scenes/Main.tscn" +config/features=PackedStringArray("4.0", "GL Compatibility") +config/icon="res://icon.svg" + +[dotnet] + +project/assembly_name="Inventory" + +[editor] + +version_control/plugin_name="GitPlugin" +version_control/autoload_on_startup=true + +[input] + +move_forward={ +"deadzone": 0.1, +"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) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null) +] +} +move_backward={ +"deadzone": 0.1, +"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) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":1.0,"script":null) +] +} +move_strafe_left={ +"deadzone": 0.1, +"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) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null) +] +} +move_strafe_right={ +"deadzone": 0.1, +"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) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null) +] +} +move_upward={ +"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":32,"key_label":0,"unicode":32,"echo":false,"script":null) +] +} +move_downward={ +"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":4194326,"key_label":0,"unicode":0,"echo":false,"script":null) +] +} +move_sprint={ +"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":4194325,"key_label":0,"unicode":0,"echo":false,"script":null) +] +} +move_jump={ +"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":32,"key_label":0,"unicode":32,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null) +] +} +cycle_movement_mode={ +"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":4194336,"key_label":0,"unicode":0,"echo":false,"script":null) +] +} +look_up={ +"deadzone": 0.1, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":3,"axis_value":-1.0,"script":null) +] +} +look_down={ +"deadzone": 0.1, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":3,"axis_value":1.0,"script":null) +] +} +look_left={ +"deadzone": 0.1, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":2,"axis_value":-1.0,"script":null) +] +} +look_right={ +"deadzone": 0.1, +"events": [Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":2,"axis_value":1.0,"script":null) +] +} +interact_pickup={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(134, 20),"global_position":Vector2(138, 63),"factor":1.0,"button_index":1,"pressed":true,"double_click":false,"script":null) +] +} +interact_place={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(119, 9),"global_position":Vector2(123, 52),"factor":1.0,"button_index":2,"pressed":true,"double_click":false,"script":null) +] +} + +[layer_names] + +3d_render/layer_1="Default" +3d_render/layer_2="Outline" +3d_physics/layer_1="World" +3d_physics/layer_2="Player" +3d_physics/layer_3="Item" + +[physics] + +common/physics_ticks_per_second=120 diff --git a/scenes/Main.tscn b/scenes/Main.tscn new file mode 100644 index 0000000..19eeba2 --- /dev/null +++ b/scenes/Main.tscn @@ -0,0 +1,33 @@ +[gd_scene load_steps=5 format=3] + +[ext_resource type="PackedScene" uid="uid://b7o2y54duqxft" path="res://scenes/World.tscn" id="1_px6sj"] +[ext_resource type="Shader" path="res://shaders/outline.gdshader" id="2_dd8cf"] +[ext_resource type="Script" path="res://scripts/CloneMainCamera.gd" id="2_m7qgc"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_7rvwk"] +shader = ExtResource("2_dd8cf") +shader_parameter/line_color = Color(1, 1, 1, 0.75) +shader_parameter/line_thickness = 2.0 + +[node name="Main" type="Node"] + +[node name="World" parent="." instance=ExtResource("1_px6sj")] + +[node name="SubViewportContainer" type="SubViewportContainer" parent="."] +material = SubResource("ShaderMaterial_7rvwk") +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +stretch = true + +[node name="SubViewport" type="SubViewport" parent="SubViewportContainer"] +transparent_bg = true +handle_input_locally = false +size = Vector2i(1152, 648) +render_target_update_mode = 4 + +[node name="ClonedCamera" type="Camera3D" parent="SubViewportContainer/SubViewport"] +cull_mask = 2 +script = ExtResource("2_m7qgc") diff --git a/scenes/Player.tscn b/scenes/Player.tscn new file mode 100644 index 0000000..64e9008 --- /dev/null +++ b/scenes/Player.tscn @@ -0,0 +1,148 @@ +[gd_scene load_steps=13 format=3 uid="uid://bgymmkpfgntea"] + +[ext_resource type="Script" path="res://scripts/Player.gd" id="1_kae1r"] +[ext_resource type="Script" path="res://scripts/Camera.gd" id="3_wo4mm"] +[ext_resource type="PackedScene" uid="uid://brbatwk2mtko4" path="res://assets/character.glb" id="4_e6k8n"] +[ext_resource type="Script" path="res://scripts/AnimationController.gd" id="4_y3wch"] +[ext_resource type="Script" path="res://scripts/PickupController.gd" id="5_wmkdb"] + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_2p66e"] +radius = 0.3 +height = 1.5 + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_30188"] +animation = &"idle" + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_hov4s"] +animation = &"walk_backward" + +[sub_resource type="AnimationNodeTransition" id="AnimationNodeTransition_881to"] +sync = true +xfade_time = 0.1 +input_0/name = "forward" +input_0/auto_advance = false +input_0/reset = true +input_1/name = "backward" +input_1/auto_advance = false +input_1/reset = true + +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_rer12"] +animation = &"walk_forward" + +[sub_resource type="AnimationNodeTransition" id="AnimationNodeTransition_5e4h6"] +xfade_time = 0.25 +input_0/name = "idle" +input_0/auto_advance = false +input_0/reset = false +input_1/name = "move" +input_1/auto_advance = false +input_1/reset = true + +[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_bgirn"] +nodes/idle/node = SubResource("AnimationNodeAnimation_30188") +nodes/idle/position = Vector2(240, 180) +nodes/output/position = Vector2(640, 280) +nodes/walk_backward/node = SubResource("AnimationNodeAnimation_hov4s") +nodes/walk_backward/position = Vector2(0, 420) +nodes/walk_direction/node = SubResource("AnimationNodeTransition_881to") +nodes/walk_direction/position = Vector2(220, 320) +nodes/walk_forward/node = SubResource("AnimationNodeAnimation_rer12") +nodes/walk_forward/position = Vector2(0, 280) +nodes/walk_state/node = SubResource("AnimationNodeTransition_5e4h6") +nodes/walk_state/position = Vector2(440, 220) +node_connections = [&"output", 0, &"walk_state", &"walk_direction", 0, &"walk_forward", &"walk_direction", 1, &"walk_backward", &"walk_state", 0, &"idle", &"walk_state", 1, &"walk_direction"] + +[node name="Player" type="CharacterBody3D" node_paths=PackedStringArray("camera")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.75, 0) +collision_layer = 2 +script = ExtResource("1_kae1r") +camera = NodePath("Character/Skeleton/Root/LowerBody/UpperBody/Neck/Head/Camera") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +shape = SubResource("CapsuleShape3D_2p66e") + +[node name="Character" parent="." instance=ExtResource("4_e6k8n")] +transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, -0.75, 0) + +[node name="Root" type="BoneAttachment3D" parent="Character/Skeleton" index="0"] +transform = Transform3D(-1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0.199674, 0) +bone_name = "Root" +bone_idx = 0 +use_external_skeleton = true +external_skeleton = NodePath("../Skeleton3D") + +[node name="LowerBody" type="BoneAttachment3D" parent="Character/Skeleton/Root"] +transform = Transform3D(-1, -3.0926e-10, 8.74222e-08, 8.71645e-08, 0.0732855, 0.997311, -6.71521e-09, 0.997311, -0.0732856, 0, 0, -0.199593) +bone_name = "LowerBody" +bone_idx = 1 +use_external_skeleton = true +external_skeleton = NodePath("../../Skeleton3D") + +[node name="UpperBody" type="BoneAttachment3D" parent="Character/Skeleton/Root/LowerBody"] +transform = Transform3D(1, -1.02448e-08, 8.68204e-08, 4.44089e-16, 0.99311, 0.117186, -8.74228e-08, -0.117186, 0.99311, 0, 0.154362, -7.45058e-09) +bone_name = "UpperBody" +bone_idx = 2 +use_external_skeleton = true +external_skeleton = NodePath("../../../Skeleton3D") + +[node name="Neck" type="BoneAttachment3D" parent="Character/Skeleton/Root/LowerBody/UpperBody"] +transform = Transform3D(1, -1.66533e-16, 0, 0, 0.998891, -0.0470728, -7.10543e-15, 0.0470728, 0.998892, -1.11022e-16, 0.251888, 5.58794e-09) +bone_name = "Neck" +bone_idx = 3 +use_external_skeleton = true +external_skeleton = NodePath("../../../../Skeleton3D") + +[node name="Head" type="BoneAttachment3D" parent="Character/Skeleton/Root/LowerBody/UpperBody/Neck"] +transform = Transform3D(-1, 1.20146e-10, 8.74227e-08, 8.74228e-08, 0.0013743, 0.999999, -7.10543e-15, 0.999999, -0.00137436, -2.22045e-16, 0.101598, 3.72529e-09) +bone_name = "Head" +bone_idx = 4 +use_external_skeleton = true +external_skeleton = NodePath("../../../../../Skeleton3D") + +[node name="Camera" type="Camera3D" parent="Character/Skeleton/Root/LowerBody/UpperBody/Neck/Head"] +transform = Transform3D(1, -1.67666e-09, 1.78995e-09, 1.82243e-09, 0.0195853, -0.999808, 1.6413e-09, 0.999808, 0.0195851, 6.66283e-09, 0.0576435, 0.0321114) +cull_mask = 1 +script = ExtResource("3_wo4mm") + +[node name="Skeleton3D" parent="Character/Skeleton" index="1"] +bones/0/position = Vector3(0, 0.199674, 0) +bones/1/rotation = Quaternion(2.9641e-08, 0.732559, 0.680703, 3.21262e-08) +bones/2/rotation = Quaternion(-0.0586944, 4.3636e-08, 2.56562e-09, 0.998276) +bones/4/rotation = Quaternion(3.09298e-08, 0.707592, 0.706621, 3.08874e-08) +bones/5/rotation = Quaternion(-0.00410346, -0.0231825, 0.984812, -0.172018) +bones/6/rotation = Quaternion(1.52056e-05, 5.1329e-05, -0.0647817, 0.997899) +bones/7/rotation = Quaternion(-1.1035e-05, -5.20782e-05, -0.0156829, 0.999877) +bones/12/rotation = Quaternion(0.00410346, -0.0231825, 0.984812, 0.172018) +bones/13/rotation = Quaternion(1.52056e-05, -5.1329e-05, 0.0647817, 0.997899) +bones/14/rotation = Quaternion(-1.1035e-05, 5.20782e-05, 0.0156829, 0.999877) +bones/19/rotation = Quaternion(0.0101145, -0.706764, 0.707308, 0.00993137) +bones/20/rotation = Quaternion(0.026743, 0.00254827, 0.0016329, 0.999638) +bones/21/rotation = Quaternion(-0.00105386, -0.689614, 0.723924, -0.0191383) +bones/22/rotation = Quaternion(-0.0101145, -0.706764, 0.707308, -0.00993136) +bones/23/rotation = Quaternion(0.026743, -0.00254827, -0.0016329, 0.999638) +bones/24/rotation = Quaternion(0.00105386, -0.689614, 0.723924, 0.0191383) + +[node name="AnimationController" type="Node3D" parent="." node_paths=PackedStringArray("camera", "skeleton", "root_bone")] +transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0) +script = ExtResource("4_y3wch") +camera = NodePath("../Character/Skeleton/Root/LowerBody/UpperBody/Neck/Head/Camera") +skeleton = NodePath("../Character/Skeleton/Skeleton3D") +root_bone = NodePath("../Character/Skeleton/Root") + +[node name="AnimationTree" type="AnimationTree" parent="AnimationController"] +tree_root = SubResource("AnimationNodeBlendTree_bgirn") +anim_player = NodePath("../../Character/AnimationPlayer") +active = true +parameters/walk_direction/current_state = "forward" +parameters/walk_direction/transition_request = "" +parameters/walk_direction/current_index = 0 +parameters/walk_state/current_state = "idle" +parameters/walk_state/transition_request = "" +parameters/walk_state/current_index = 0 + +[node name="PickupController" type="Node3D" parent="." node_paths=PackedStringArray("camera")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.324461, -0.291141) +script = ExtResource("5_wmkdb") +camera = NodePath("../Character/Skeleton/Root/LowerBody/UpperBody/Neck/Head/Camera") + +[editable path="Character"] diff --git a/scenes/World.tscn b/scenes/World.tscn new file mode 100644 index 0000000..dac9d99 --- /dev/null +++ b/scenes/World.tscn @@ -0,0 +1,185 @@ +[gd_scene load_steps=19 format=3 uid="uid://b7o2y54duqxft"] + +[ext_resource type="Texture2D" uid="uid://bmormqcp2qv4k" path="res://assets/terrain_grass.png" id="1_mi8br"] +[ext_resource type="PackedScene" uid="uid://bgymmkpfgntea" path="res://scenes/Player.tscn" id="2_4nlbn"] +[ext_resource type="Script" path="res://scripts/Item.gd" id="3_2aosx"] + +[sub_resource type="WorldBoundaryShape3D" id="WorldBoundaryShape3D_4q4bb"] + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_eyy3t"] +albedo_texture = ExtResource("1_mi8br") +uv1_scale = Vector3(8, 8, 8) +texture_filter = 0 + +[sub_resource type="PlaneMesh" id="PlaneMesh_ce3ji"] +material = SubResource("StandardMaterial3D_eyy3t") +size = Vector2(256, 256) + +[sub_resource type="BoxShape3D" id="BoxShape3D_1w5kh"] +size = Vector3(2, 0.1, 1) + +[sub_resource type="BoxMesh" id="BoxMesh_jergk"] +size = Vector3(2, 0.1, 1) + +[sub_resource type="BoxMesh" id="BoxMesh_yyb1u"] + +[sub_resource type="BoxShape3D" id="BoxShape3D_cx0e5"] +size = Vector3(0.05, 0.2, 0.05) + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_4jkru"] +albedo_color = Color(1, 0.364706, 1, 1) + +[sub_resource type="BoxMesh" id="BoxMesh_2cpu0"] +material = SubResource("StandardMaterial3D_4jkru") +size = Vector3(0.05, 0.2, 0.05) + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_2pcea"] +albedo_color = Color(1, 0, 0, 1) + +[sub_resource type="CylinderMesh" id="CylinderMesh_t8qr8"] +material = SubResource("StandardMaterial3D_2pcea") +top_radius = 0.01 +bottom_radius = 0.01 +height = 0.1 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_befdk"] +albedo_color = Color(0, 1, 0, 1) + +[sub_resource type="CylinderMesh" id="CylinderMesh_kgvwt"] +material = SubResource("StandardMaterial3D_befdk") +top_radius = 0.01 +bottom_radius = 0.01 +height = 0.1 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_gx7gw"] +albedo_color = Color(0, 0, 1, 1) + +[sub_resource type="CylinderMesh" id="CylinderMesh_8iq4p"] +material = SubResource("StandardMaterial3D_gx7gw") +top_radius = 0.01 +bottom_radius = 0.01 +height = 0.1 + +[node name="World" type="Node3D"] + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +transform = Transform3D(0.56961, 0.533344, -0.625371, -2.77304e-08, 0.760871, 0.648904, 0.821916, -0.369622, 0.433399, 0, 4, 0) +light_color = Color(0.968627, 0.858824, 0.717647, 1) +shadow_enabled = true + +[node name="Floor" type="StaticBody3D" parent="."] +collision_mask = 0 +metadata/_edit_lock_ = true + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Floor"] +shape = SubResource("WorldBoundaryShape3D_4q4bb") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Floor"] +mesh = SubResource("PlaneMesh_ce3ji") + +[node name="Player" parent="." instance=ExtResource("2_4nlbn")] + +[node name="Table" type="Node3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -2) +metadata/_edit_lock_ = true + +[node name="Box" type="StaticBody3D" parent="Table"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Table/Box"] +shape = SubResource("BoxShape3D_1w5kh") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Table/Box"] +mesh = SubResource("BoxMesh_jergk") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Table"] +transform = Transform3D(0.099, 0, -8.12102e-05, 0, 1, 0, 8.12102e-05, 0, 0.099, -0.900327, 0.5, 0.399261) +mesh = SubResource("BoxMesh_yyb1u") +skeleton = NodePath("") + +[node name="MeshInstance3D2" type="MeshInstance3D" parent="Table"] +transform = Transform3D(0.099, 0, -8.12102e-05, 0, 1, 0, 8.12102e-05, 0, 0.099, -0.899678, 0.5, -0.392738) +mesh = SubResource("BoxMesh_yyb1u") +skeleton = NodePath("") + +[node name="MeshInstance3D3" type="MeshInstance3D" parent="Table"] +transform = Transform3D(0.099, 0, -8.12102e-05, 0, 1, 0, 8.12102e-05, 0, 0.099, 0.9, 0.5, 0.401) +mesh = SubResource("BoxMesh_yyb1u") +skeleton = NodePath("") + +[node name="MeshInstance3D4" type="MeshInstance3D" parent="Table"] +transform = Transform3D(0.099, 0, -8.12102e-05, 0, 1, 0, 8.12102e-05, 0, 0.099, 0.9, 0.5, -0.391) +mesh = SubResource("BoxMesh_yyb1u") +skeleton = NodePath("") + +[node name="Item" type="RigidBody3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.762804, 1.15199, -1.85385) +collision_layer = 4 +collision_mask = 7 +script = ExtResource("3_2aosx") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Item"] +shape = SubResource("BoxShape3D_cx0e5") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Item"] +mesh = SubResource("BoxMesh_2cpu0") + +[node name="X" type="MeshInstance3D" parent="Item"] +transform = Transform3D(-4.37114e-08, 1, 0, -1, -4.37114e-08, 0, 0, 0, 1, 0.1, 0, 0) +mesh = SubResource("CylinderMesh_t8qr8") + +[node name="Y" type="MeshInstance3D" parent="Item"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 0) +mesh = SubResource("CylinderMesh_kgvwt") + +[node name="Z" type="MeshInstance3D" parent="Item"] +transform = Transform3D(1.91069e-15, -4.37114e-08, -1, -1, -4.37114e-08, 0, -4.37114e-08, 1, -4.37114e-08, 0, 0, 0.1) +mesh = SubResource("CylinderMesh_8iq4p") + +[node name="Item2" type="RigidBody3D" parent="."] +transform = Transform3D(-3.79793e-08, -0.868865, 0.495049, 1, -4.37114e-08, 0, 2.16393e-08, 0.495049, 0.868865, -0.49893, 1.07653, -1.68684) +collision_layer = 4 +collision_mask = 7 +script = ExtResource("3_2aosx") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Item2"] +shape = SubResource("BoxShape3D_cx0e5") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Item2"] +mesh = SubResource("BoxMesh_2cpu0") + +[node name="X" type="MeshInstance3D" parent="Item2"] +transform = Transform3D(-4.37114e-08, 1, 0, -1, -4.37114e-08, 0, 0, 0, 1, 0.1, 0, 0) +mesh = SubResource("CylinderMesh_t8qr8") + +[node name="Y" type="MeshInstance3D" parent="Item2"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 0) +mesh = SubResource("CylinderMesh_kgvwt") + +[node name="Z" type="MeshInstance3D" parent="Item2"] +transform = Transform3D(1.91069e-15, -4.37114e-08, -1, -1, -4.37114e-08, 0, -4.37114e-08, 1, -4.37114e-08, 0, 0, 0.1) +mesh = SubResource("CylinderMesh_8iq4p") + +[node name="Item3" type="RigidBody3D" parent="."] +transform = Transform3D(0.994627, -0.103522, -4.52508e-09, 0, -4.37114e-08, 1, -0.103522, -0.994627, -4.34765e-08, -0.106395, 1.07776, -1.65152) +collision_layer = 4 +collision_mask = 7 +script = ExtResource("3_2aosx") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Item3"] +shape = SubResource("BoxShape3D_cx0e5") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="Item3"] +mesh = SubResource("BoxMesh_2cpu0") + +[node name="X" type="MeshInstance3D" parent="Item3"] +transform = Transform3D(-4.37114e-08, 1, 0, -1, -4.37114e-08, 0, 0, 0, 1, 0.1, 0, 0) +mesh = SubResource("CylinderMesh_t8qr8") + +[node name="Y" type="MeshInstance3D" parent="Item3"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 0) +mesh = SubResource("CylinderMesh_kgvwt") + +[node name="Z" type="MeshInstance3D" parent="Item3"] +transform = Transform3D(1.91069e-15, -4.37114e-08, -1, -1, -4.37114e-08, 0, -4.37114e-08, 1, -4.37114e-08, 0, 0, 0.1) +mesh = SubResource("CylinderMesh_8iq4p") diff --git a/scripts/AnimationController.gd b/scripts/AnimationController.gd new file mode 100644 index 0000000..c534ba7 --- /dev/null +++ b/scripts/AnimationController.gd @@ -0,0 +1,137 @@ +extends Node3D + +@export var camera : Camera +@export var skeleton : Skeleton3D +@export var root_bone : BoneAttachment3D + +@onready var player := find_parent("Player") as Player +@onready var anim_tree := $AnimationTree as AnimationTree +@onready var anim_player := anim_tree.get_node(anim_tree.anim_player) as AnimationPlayer + +@onready var camera_default_transform := camera.transform + +@onready var walk_forward_anim := anim_player.get_animation("walk_forward") +@onready var walk_backward_anim := anim_player.get_animation("walk_backward") + +# TODO: @onready var walk_loop_length := walk_forward_anim.length + +# Contains all the bones in the skeleton, keyed by name (e.g. "LowerArm_L"). +var bones := { } + +# Whether the player's body is currently turning to match up with the camera rotation. +var is_turning := false + +# Current amount the body is turned due to walking sideways. +var body_yaw := 0.0 # radians + + +func _ready() -> void: + var add_bone := func(bone: BoneAttachment3D) -> void: + bone.override_pose = true + bones[bone.name] = bone + + add_bone.call(root_bone) + for child in root_bone.find_children("*", "BoneAttachment3D"): + add_bone.call(child as BoneAttachment3D) + + +func _process(delta: float) -> void: + reset_transforms() + handle_turning(delta) + handle_looking_animation(delta) + handle_walking_animation(delta) + + +func reset_transforms() -> void: + for bone_name in bones: + var bone := bones[bone_name] as BoneAttachment3D + bone.transform = skeleton.get_bone_pose(bone.bone_idx) + camera.transform = camera_default_transform + + +func handle_turning(delta: float) -> void: + const TURN_BEGIN := 60.0 # Start turning when camera is rotated this much. + const TURN_END := 5.0 # Stop turning when body is this close to camera rotation. + const TURN_SPEED := 6.0 + + var yaw := camera.current_yaw # Camera yaw relative to player yaw. + if player.is_moving || abs(yaw) > deg_to_rad(TURN_BEGIN): + is_turning = true + if is_turning: + var yaw_delta = sign(yaw) * min(abs(yaw), (abs(yaw) * TURN_SPEED) * delta) + player.rotate_y(yaw_delta) + camera.current_yaw -= yaw_delta + if abs(camera.current_yaw) < deg_to_rad(TURN_END): + is_turning = false + + +func handle_looking_animation(_delta: float) -> void: + const PITCH_FACTOR_NECK := 0.25 + const PITCH_FACTOR_HEAD := 0.35 + + var pitch := camera.current_pitch + bones["Neck"].rotate_x(-pitch * PITCH_FACTOR_NECK) + bones["Head"].rotate_x(-pitch * PITCH_FACTOR_HEAD) + camera.rotate_x(pitch * (1 - PITCH_FACTOR_NECK - PITCH_FACTOR_HEAD)) + + const YAW_FACTOR_LOWER_BODY := 0.06 + const YAW_FACTOR_UPPER_BODY := 0.18 + const YAW_FACTOR_NECK := 0.2 + const YAW_FACTOR_HEAD := 0.3 + + var yaw := camera.current_yaw + bones["LowerBody"].global_rotate(Vector3.UP, yaw * YAW_FACTOR_LOWER_BODY) + bones["UpperBody"].global_rotate(Vector3.UP, yaw * YAW_FACTOR_UPPER_BODY) + bones["Neck"].global_rotate(Vector3.UP, yaw * YAW_FACTOR_NECK) + bones["Head"].global_rotate(Vector3.UP, yaw * YAW_FACTOR_HEAD) + camera.global_rotate(Vector3.UP, yaw * (1 - YAW_FACTOR_LOWER_BODY - YAW_FACTOR_UPPER_BODY - YAW_FACTOR_NECK - YAW_FACTOR_HEAD)) + + # How much of the "ideal" camera rotation (rather than animation rotation) should be applied. + const CAMERA_FACTOR_IDEAL_PITCH := 1.0 # 0.7 + const CAMERA_FACTOR_IDEAL_YAW := 1.0 # 0.8 + const CAMERA_FACTOR_IDEAL_ROLL := 1.0 # 0.9 + + var global_yaw := player.rotation.y + yaw + camera.global_rotation.x = lerp_angle(camera.global_rotation.x, pitch, CAMERA_FACTOR_IDEAL_PITCH) + camera.global_rotation.y = lerp_angle(camera.global_rotation.y, global_yaw, CAMERA_FACTOR_IDEAL_YAW) + camera.global_rotation.z = lerp_angle(camera.global_rotation.z, 0, CAMERA_FACTOR_IDEAL_ROLL) + + +func handle_walking_animation(delta: float) -> void: + var input := Input.get_vector("move_strafe_left", "move_strafe_right", "move_forward", "move_backward") + var is_on_floor := player.time_since_on_floor < 0.25 + var is_moving_forward := input.y <= 0 + + var walk_state : String + var walk_direction : String + var target_body_yaw : float + + if is_on_floor && player.is_moving: + walk_state = "move" + else: + walk_state = "idle" + + if is_moving_forward: + walk_direction = "forward" + target_body_yaw = -Vector2.UP.angle_to(input) + else: + walk_direction = "backward" + target_body_yaw = -Vector2.DOWN.angle_to(input) + + anim_tree["parameters/walk_state/transition_request"] = walk_state + anim_tree["parameters/walk_direction/transition_request"] = walk_direction + + const YAW_FACTOR_LOWER_BODY := 0.25 + const YAW_FACTOR_UPPER_BODY := 0.25 + const YAW_FACTOR_NECK := 0.50 + + body_yaw += (target_body_yaw - body_yaw) * delta * 6 + + bones["Root"].global_rotate(Vector3.UP, body_yaw) + bones["LowerBody"].global_rotate(Vector3.UP, -body_yaw * YAW_FACTOR_LOWER_BODY) + bones["UpperBody"].global_rotate(Vector3.UP, -body_yaw * YAW_FACTOR_UPPER_BODY) + bones["Neck"].global_rotate(Vector3.UP, -body_yaw * YAW_FACTOR_NECK) + + +static func angle_difference(from: float, to: float) -> float: + return fposmod(to - from + PI, TAU) - PI diff --git a/scripts/Camera.gd b/scripts/Camera.gd new file mode 100644 index 0000000..4b5fef2 --- /dev/null +++ b/scripts/Camera.gd @@ -0,0 +1,45 @@ +class_name Camera +extends Camera3D + +@export var mouse_sensitivity := Vector2(0.2, 0.2) # degrees per pixel +@export var joystick_sensitivity := Vector2(200, 200) # degrees per second +@export var pitch_min := -80.0 +@export var pitch_max := 80.0 + +var current_pitch := 0.0 +var current_yaw := 0.0 + + +func _unhandled_input(event: InputEvent) -> void: + # When pressing escape and mouse is currently captured, release it. + if event.is_action_pressed("ui_cancel") && is_mouse_captured(): + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + + var button := event as InputEventMouseButton + if button: + # Grab the mouse when pressing the primary mouse button. + # TODO: Make "primary mouse button" configurable. + if event.button_index == MOUSE_BUTTON_LEFT: + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + + var mouseMotion := event as InputEventMouseMotion + if mouseMotion && is_mouse_captured(): + apply_rotation(-mouseMotion.relative * mouse_sensitivity) + + +func _process(delta: float) -> void: + # Handle joystick camera controls. + var input := Input.get_vector("look_left", "look_right", "look_up", "look_down") + apply_rotation(-input * joystick_sensitivity * delta) + + +# Returns whether the mouse is currently captured by the game. +func is_mouse_captured() -> bool: + return Input.mouse_mode == Input.MOUSE_MODE_CAPTURED + + +# Applies the specified rotation (in degrees) to the camera. +func apply_rotation(delta: Vector2) -> void: + delta = delta / 360 * TAU + current_pitch = clampf(current_pitch + delta.y, deg_to_rad(pitch_min), deg_to_rad(pitch_max)) + current_yaw += delta.x diff --git a/scripts/CloneMainCamera.gd b/scripts/CloneMainCamera.gd new file mode 100644 index 0000000..0da9a03 --- /dev/null +++ b/scripts/CloneMainCamera.gd @@ -0,0 +1,6 @@ +extends Camera3D + +func _process(_delta: float) -> void: + var main_viewport := get_tree().root.get_viewport() + var main_camera := main_viewport.get_camera_3d() + global_transform = main_camera.global_transform diff --git a/scripts/Item.gd b/scripts/Item.gd new file mode 100644 index 0000000..b73d5e2 --- /dev/null +++ b/scripts/Item.gd @@ -0,0 +1,8 @@ +extends RigidBody3D +class_name Item + +const GRID_SIZE := 0.05 + +@export var size := Vector3i(1, 4, 1) + +@onready var mesh := $MeshInstance3D as MeshInstance3D diff --git a/scripts/PhysicsLayer.gd b/scripts/PhysicsLayer.gd new file mode 100644 index 0000000..8b9c77d --- /dev/null +++ b/scripts/PhysicsLayer.gd @@ -0,0 +1,7 @@ +class_name PhysicsLayer + +enum { + WORLD = 1 << 0, + PLAYER = 1 << 1, + ITEM = 1 << 2, +} diff --git a/scripts/PickupController.gd b/scripts/PickupController.gd new file mode 100644 index 0000000..3016543 --- /dev/null +++ b/scripts/PickupController.gd @@ -0,0 +1,119 @@ +extends Node3D + +@export var camera : Camera3D +@export var pickup_distance := 2.0 + +@onready var world := find_parent("World") as Node3D + +var current_item : Item +var is_current_item_held := false +var placement_preview : MeshInstance3D +# TODO: Support holding multiple items. +# TODO: Allow rotation of the item while held. + + +func _ready() -> void: + pass + + +func _input(event: InputEvent) -> void: + ensure_current_item_valid() + if !current_item: return + + if event.is_action_pressed("interact_pickup"): + if !is_current_item_held: + is_current_item_held = true + + # Create a clone of the item's mesh and use it as a placement preview. + placement_preview = current_item.get_node("MeshInstance3D").duplicate(0) as MeshInstance3D + placement_preview.name = "PlacementPreview" + placement_preview.layers = RenderLayer.OUTLINE + placement_preview.top_level = true + add_child(placement_preview) + + # Parent item to the pickup controller. + var prev_rot := current_item.global_rotation + current_item.get_parent().remove_child(current_item) + add_child(current_item) + current_item.mesh.layers &= ~RenderLayer.OUTLINE + current_item.position = Vector3.ZERO + current_item.global_rotation = prev_rot + current_item.freeze = true + + get_viewport().set_input_as_handled() + + elif event.is_action_pressed("interact_place"): + if is_current_item_held: + is_current_item_held = false + + # Parent item back to the world. + remove_child(current_item) + world.add_child(current_item) + + # Set item's transform to where the placement preview is. + # TODO: If placement preview is not valid, don't allow placing the item. + current_item.global_transform = placement_preview.global_transform + current_item.freeze = false + + placement_preview.queue_free() + placement_preview = null + + get_viewport().set_input_as_handled() + + +func _process(_delta: float) -> void: + pass + + +func _physics_process(_delta: float) -> void: + ensure_current_item_valid() + + if is_current_item_held: + # Cast a ray but exlude the current item from being hit. + var ray_result := ray_to_mouse_cursor([ current_item ]) + if ray_result: + var pos := ray_result.position as Vector3 + var normal := ray_result.normal as Vector3 + + # Snap rotation to nearest axis. + var global_rot = current_item.global_rotation + global_rot.x = snappedf(global_rot.x, TAU / 4) + global_rot.y = snappedf(global_rot.y, TAU / 4) + global_rot.z = snappedf(global_rot.z, TAU / 4) + placement_preview.global_rotation = global_rot + + # Snap the position to the grid. + var half_size := current_item.size * Item.GRID_SIZE / 2 + pos += half_size * (normal * placement_preview.global_transform.basis) + pos = pos.snapped(Item.GRID_SIZE * Vector3.ONE) + placement_preview.global_position = pos + + else: + # Remove the outline from the previously looked-at item. + if current_item: + current_item.mesh.layers &= ~RenderLayer.OUTLINE + + var ray_result := ray_to_mouse_cursor() + # If the ray hit anything and the object hit is an item, set it as current. + if ray_result: + current_item = ray_result.collider as Item + if current_item: + current_item.mesh.layers |= RenderLayer.OUTLINE + + +func ensure_current_item_valid() -> void: + if !current_item: return + if !is_instance_valid(current_item): + current_item = null + is_current_item_held = false + placement_preview.queue_free() + +func ray_to_mouse_cursor(exclude: Array[CollisionObject3D] = []) -> Dictionary: + const COLLIDE_WITH := PhysicsLayer.WORLD | PhysicsLayer.ITEM + var map_to_rid := func(obj: CollisionObject3D) -> RID: return obj.get_rid() + + var mouse := get_viewport().get_mouse_position() + var from := camera.project_ray_origin(mouse) + var to := from + camera.project_ray_normal(mouse) * pickup_distance + var query := PhysicsRayQueryParameters3D.create(from, to, COLLIDE_WITH, exclude.map(map_to_rid)) + return get_world_3d().direct_space_state.intersect_ray(query) diff --git a/scripts/Player.gd b/scripts/Player.gd new file mode 100644 index 0000000..d61c851 --- /dev/null +++ b/scripts/Player.gd @@ -0,0 +1,123 @@ +class_name Player +extends CharacterBody3D + +@export var camera: Camera + +@export_group("Movement") +@export var move_accel := 6.0 +@export var move_max_speed := 4.0 +@export var friction_floor := 12.0 +@export var friction_air := 2.0 +@export var gravity := -12.0 + +@export_group("Jumping") +@export var jump_velocity := 5.0 +@export var jump_early_time := 0.0 # Time (in seconds) after pressing the jump button a jump may occur late. +@export var jump_coyote_time := 0.0 # Time (in seconds) after leaving a jumpable surface when a jump may still occur. + +enum MovementModeEnum { DEFAULT, FLYING, NO_CLIP } +var movement_mode := MovementModeEnum.DEFAULT + +var is_moving := false +var is_sprinting := false + +var time_since_jump_pressed := INF +var time_since_on_floor := INF + + +func _input(event: InputEvent) -> void: + # Inputs that are valid when the game is focused. + # =============================================== + + if event.is_action("move_sprint"): + is_sprinting = event.is_pressed() + get_viewport().set_input_as_handled() + + if event.is_action_pressed("move_jump"): + time_since_jump_pressed = 0 + get_viewport().set_input_as_handled() + + # Cycle movement mode between default, flying and noclip. + if event.is_action_pressed("cycle_movement_mode"): + if (+movement_mode > MovementModeEnum.NO_CLIP): + movement_mode = MovementModeEnum.DEFAULT; + get_viewport().set_input_as_handled() + + # Inputs that are valid only when the mouse is captured. + # ====================================================== + if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: + pass + + +func _physics_process(delta: float) -> void: + match movement_mode: + MovementModeEnum.DEFAULT: + process_movement_default(delta) + MovementModeEnum.FLYING: + process_movement_flying(delta, false) + MovementModeEnum.NO_CLIP: + process_movement_flying(delta, true) + + +func process_movement_default(delta: float) -> void: + var input := Input.get_vector("move_strafe_left", "move_strafe_right", "move_forward", "move_backward") + + velocity.y += gravity * delta + var hor_vel := Vector3(velocity.x, 0, velocity.z) + + var target := basis.rotated(Vector3.UP, camera.current_yaw) * Vector3(input.x, 0, input.y) * move_max_speed + is_moving = target.dot(hor_vel) > 0 + + var accel: float + if is_moving: accel = move_accel + elif is_on_floor(): accel = friction_floor + else: accel = friction_air + + if is_sprinting: + target *= 5 + accel *= 5 + + hor_vel = hor_vel.lerp(target, accel * delta) + velocity = Vector3(hor_vel.x, velocity.y, hor_vel.z) + + # TODO: Check if this still applies. + # Sometimes, when pushing into a wall, jumping wasn't working. + # Possibly due to `IsOnFloor` returning `false` for some reason. + # The `JumpEarlyTime` feature seems to avoid this issue, thankfully. + + if is_on_floor(): time_since_on_floor = 0 + else: time_since_on_floor += delta + + if time_since_jump_pressed <= jump_early_time && time_since_on_floor <= jump_coyote_time: + velocity.y = jump_velocity + time_since_jump_pressed = INF + time_since_on_floor = INF + + move_and_slide() + +func process_movement_flying(delta: float, no_clip: bool) -> void: + var input := Vector3( + Input.get_axis("move_strafe_left", "move_strafe_right"), + Input.get_axis("move_downward", "move_upward"), + Input.get_axis("move_forward", "move_backward")) + + velocity *= 1 - friction_air * delta; + + var target := camera.global_transform.basis.get_rotation_quaternion() * input * move_max_speed + is_moving = target.dot(velocity) > 0 + + var accel: float + if is_moving: accel = move_accel + else: accel = friction_air + + target *= 4 + accel *= 4 + + if is_sprinting: + target *= 5 + accel *= 5 + + velocity = velocity.lerp(target, accel * delta) + + if no_clip: translate(velocity * delta) + else: move_and_slide() diff --git a/scripts/RenderLayer.gd b/scripts/RenderLayer.gd new file mode 100644 index 0000000..b111d80 --- /dev/null +++ b/scripts/RenderLayer.gd @@ -0,0 +1,6 @@ +class_name RenderLayer + +enum { + DEFAULT = 1 << 0, + OUTLINE = 1 << 1, +} diff --git a/shaders/outline.gdshader b/shaders/outline.gdshader new file mode 100644 index 0000000..0cfd0f2 --- /dev/null +++ b/shaders/outline.gdshader @@ -0,0 +1,24 @@ +// Adapted from Leafshade Interactive's outline shader +// explained and shown in https://youtu.be/yh1Kdr37RmI. + +shader_type canvas_item; + +uniform vec4 line_color : source_color = vec4(1.0); +uniform float line_thickness : hint_range(0.0, 10.0) = 2.0; + +void fragment() { + vec2 size = TEXTURE_PIXEL_SIZE * line_thickness; + + float outline = + texture(TEXTURE, UV + vec2( size.x, 0)).a + + texture(TEXTURE, UV + vec2(-size.x, 0)).a + + texture(TEXTURE, UV + vec2(0, size.y)).a + + texture(TEXTURE, UV + vec2(0, -size.y)).a + + texture(TEXTURE, UV + vec2( size.x, size.y)).a + + texture(TEXTURE, UV + vec2(-size.x, size.y)).a + + texture(TEXTURE, UV + vec2(-size.x, -size.y)).a + + texture(TEXTURE, UV + vec2( size.x, -size.y)).a; + + float alpha = min(outline, 1.0) - texture(TEXTURE, UV).a; + COLOR = vec4(line_color.rgb, line_color.a * alpha); +}