commit
b656d6e772
23 changed files with 1234 additions and 0 deletions
@ -0,0 +1,2 @@ |
||||
# Normalize EOL for all files that Git considers text files. |
||||
* text=auto eol=lf |
@ -0,0 +1,8 @@ |
||||
# Godot 4+ specific ignore. |
||||
.godot/ |
||||
|
||||
# Don't include git addon. |
||||
addons/godot-git-plugin/ |
||||
|
||||
# Ignore blender backups. |
||||
*.blend[0-9] |
@ -0,0 +1,158 @@ |
||||
using Godot; |
||||
using System; |
||||
|
||||
public partial class Player : CharacterBody3D |
||||
{ |
||||
public float MouseSensitivity { get; set; } = 0.2F; |
||||
|
||||
/// <summary> Time after pressing the jump button a jump may occur late. </summary> |
||||
public TimeSpan JumpEarlyTime { get; set; } = TimeSpan.FromSeconds(0.2); |
||||
|
||||
/// <summary> Time after leaving a jumpable surface when a jump may still occur. </summary> |
||||
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<Node3D>("Neck"); |
||||
_headBone = GetNode<Node3D>("Neck/Head"); |
||||
_camera = GetNode<Camera3D>("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(); |
||||
} |
||||
} |
Binary file not shown.
Binary file not shown.
@ -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 |
After Width: | Height: | Size: 2.9 KiB |
@ -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 |
@ -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 |
@ -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 |
@ -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") |
@ -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"] |
@ -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") |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -0,0 +1,7 @@ |
||||
class_name PhysicsLayer |
||||
|
||||
enum { |
||||
WORLD = 1 << 0, |
||||
PLAYER = 1 << 1, |
||||
ITEM = 1 << 2, |
||||
} |
@ -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) |
@ -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() |
@ -0,0 +1,6 @@ |
||||
class_name RenderLayer |
||||
|
||||
enum { |
||||
DEFAULT = 1 << 0, |
||||
OUTLINE = 1 << 1, |
||||
} |
@ -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); |
||||
} |
Loading…
Reference in new issue