├── project └── addons │ └── StairsCharacter │ ├── bin │ └── .gitignore │ ├── StairsCharacter.gdextension.uid │ ├── stairs_character.png │ ├── stairs_character.svg │ ├── StairsCharacter.gdextension │ └── stairs_character.svg.import ├── .gitattributes ├── .gitmodules ├── src ├── StairsCharacter.windows.template_debug.x86_64.obj ├── register_types.windows.template_debug.x86_64.obj ├── StairsCharacter.windows.template_release.x86_64.obj ├── register_types.windows.template_release.x86_64.obj ├── register_types.h ├── register_types.cpp ├── StairsCharacter.hpp └── StairsCharacter.cpp ├── .gitignore ├── LICENSE ├── SConstruct ├── README.md └── .github └── workflows └── build.yml /project/addons/StairsCharacter/bin/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /project/addons/StairsCharacter/StairsCharacter.gdextension.uid: -------------------------------------------------------------------------------- 1 | uid://tkege8xp3mnb 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "godot-cpp"] 2 | path = godot-cpp 3 | url = https://github.com/godotengine/godot-cpp.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /project/addons/StairsCharacter/stairs_character.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrjshzk/StairsCharacter3D/HEAD/project/addons/StairsCharacter/stairs_character.png -------------------------------------------------------------------------------- /src/StairsCharacter.windows.template_debug.x86_64.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrjshzk/StairsCharacter3D/HEAD/src/StairsCharacter.windows.template_debug.x86_64.obj -------------------------------------------------------------------------------- /src/register_types.windows.template_debug.x86_64.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrjshzk/StairsCharacter3D/HEAD/src/register_types.windows.template_debug.x86_64.obj -------------------------------------------------------------------------------- /src/StairsCharacter.windows.template_release.x86_64.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrjshzk/StairsCharacter3D/HEAD/src/StairsCharacter.windows.template_release.x86_64.obj -------------------------------------------------------------------------------- /src/register_types.windows.template_release.x86_64.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrjshzk/StairsCharacter3D/HEAD/src/register_types.windows.template_release.x86_64.obj -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Objects. 2 | .scons-cache/ 3 | *.os 4 | 5 | # SConstruct 6 | .sconf_temp 7 | .sconsign.dblite 8 | *.pyc 9 | 10 | # MacOS 11 | .DS_Store 12 | 13 | # Editors 14 | .vscode/ 15 | -------------------------------------------------------------------------------- /src/register_types.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | void gdextension_initialize(godot::ModuleInitializationLevel p_level); 6 | void gdextension_terminate(godot::ModuleInitializationLevel p_level); 7 | -------------------------------------------------------------------------------- /project/addons/StairsCharacter/stairs_character.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/register_types.cpp: -------------------------------------------------------------------------------- 1 | #include "register_types.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "StairsCharacter.hpp" 7 | 8 | 9 | void gdextension_initialize(ModuleInitializationLevel p_level) 10 | { 11 | if (p_level == MODULE_INITIALIZATION_LEVEL_SCENE) 12 | { 13 | ClassDB::register_class(); 14 | } 15 | } 16 | 17 | void gdextension_terminate(ModuleInitializationLevel p_level) 18 | { 19 | 20 | } 21 | 22 | extern "C" 23 | { 24 | GDExtensionBool GDE_EXPORT gdextension_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) 25 | { 26 | godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); 27 | 28 | init_obj.register_initializer(gdextension_initialize); 29 | init_obj.register_terminator(gdextension_terminate); 30 | init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE); 31 | 32 | return init_obj.init(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /project/addons/StairsCharacter/StairsCharacter.gdextension: -------------------------------------------------------------------------------- 1 | [configuration] 2 | 3 | entry_symbol = "gdextension_init" 4 | compatibility_minimum = 4.3 5 | 6 | [libraries] 7 | 8 | linux.debug.x86_64 = "bin/libStairsCharacter.linux.debug.x86_64.so" 9 | linux.release.x86_64 = "bin/libStairsCharacter.linux.release.x86_64.so" 10 | linux.debug.arm64 = "bin/libStairsCharacter.linux.debug.arm64.so" 11 | linux.release.arm64 = "bin/libStairsCharacter.linux.release.arm64.so" 12 | linux.debug.rv64 = "bin/libStairsCharacter.linux.debug.rv64.so" 13 | linux.release.rv64 = "bin/libStairsCharacter.linux.release.rv64.so" 14 | windows.debug.x86_64 = "bin/libStairsCharacter.windows.debug.x86_64.dll" 15 | windows.release.x86_64 = "bin/libStairsCharacter.windows.release.x86_64.dll" 16 | macos.debug = "bin/libStairsCharacter.macos.debug.framework" 17 | macos.release = "bin/libStairsCharacter.macos.release.framework" 18 | android.debug.arm64 = "bin/libStairsCharacter.android.debug.arm64.so" 19 | android.release.arm64 = "bin/libStairsCharacter.android.release.arm64.so" 20 | 21 | [icons] 22 | StairsCharacter3D = "stairs_character.svg" -------------------------------------------------------------------------------- /project/addons/StairsCharacter/stairs_character.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://f8rpbcuiicd7" 6 | path="res://.godot/imported/stairs_character.svg-a9660f3f5b9588e29c90e9a9aaadbbda.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/StairsCharacter/stairs_character.svg" 14 | dest_files=["res://.godot/imported/stairs_character.svg-a9660f3f5b9588e29c90e9a9aaadbbda.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /src/StairsCharacter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | using namespace godot; 8 | 9 | class StairsCharacter3D : public CharacterBody3D 10 | { 11 | GDCLASS(StairsCharacter3D, CharacterBody3D); 12 | 13 | private: 14 | NodePath collider; 15 | float step_height_up = 0.5; 16 | float step_height_down = 0.5; 17 | 18 | bool grounded; 19 | bool was_grounded; 20 | 21 | Vector3 desired_velocity; 22 | Vector3 horizontal = Vector3(1,0,1); 23 | 24 | void set_collider(const NodePath &p_collider); 25 | NodePath get_collider() const; 26 | 27 | protected: 28 | static void _bind_methods(); 29 | 30 | void set_desired_velocity(const Vector3 vel) { 31 | desired_velocity = vel; 32 | } 33 | 34 | Vector3 get_desired_velocity() const { 35 | return desired_velocity; 36 | } 37 | 38 | public: 39 | void _ready() override; 40 | void reset_grounded(); 41 | 42 | void move_and_stair_step(); 43 | void stair_step_up(); 44 | void stair_step_down(); 45 | 46 | void set_step_height_up(const float height) { 47 | step_height_up = height; 48 | } 49 | 50 | float get_step_height_up() const { 51 | return step_height_up; 52 | } 53 | 54 | void set_step_height_down(const float height) { 55 | step_height_down = height; 56 | } 57 | 58 | float get_step_height_down() const { 59 | return step_height_down; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /SConstruct: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from glob import glob 4 | from pathlib import Path 5 | 6 | # TODO: Do not copy environment after godot-cpp/test is updated . 7 | env = SConscript("godot-cpp/SConstruct") 8 | 9 | # Add source files. 10 | env.Append(CPPPATH=["src/"]) 11 | sources = Glob("src/*.cpp") 12 | 13 | # Find gdextension path even if the directory or extension is renamed (e.g. project/addons/example/example.gdextension). 14 | (extension_path,) = glob("project/addons/*/*.gdextension") 15 | 16 | # Get the addon path (e.g. project/addons/example). 17 | addon_path = Path(extension_path).parent 18 | 19 | # Get the project name from the gdextension file (e.g. example). 20 | project_name = Path(extension_path).stem 21 | 22 | scons_cache_path = os.environ.get("SCONS_CACHE") 23 | if scons_cache_path != None: 24 | CacheDir(scons_cache_path) 25 | print("Scons cache enabled... (path: '" + scons_cache_path + "')") 26 | 27 | # Create the library target (e.g. libexample.linux.debug.x86_64.so). 28 | debug_or_release = "release" if env["target"] == "template_release" else "debug" 29 | if env["platform"] == "macos": 30 | library = env.SharedLibrary( 31 | "{0}/bin/lib{1}.{2}.{3}.framework/{1}.{2}.{3}".format( 32 | addon_path, 33 | project_name, 34 | env["platform"], 35 | debug_or_release, 36 | ), 37 | source=sources, 38 | ) 39 | else: 40 | library = env.SharedLibrary( 41 | "{}/bin/lib{}.{}.{}.{}{}".format( 42 | addon_path, 43 | project_name, 44 | env["platform"], 45 | debug_or_release, 46 | env["arch"], 47 | env["SHLIBSUFFIX"], 48 | ), 49 | source=sources, 50 | ) 51 | 52 | Default(library) 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stairs Character 3D 2 | 3 | A simple class based on Godot's default CharacterBody3D with very simple stair stepping ability. 4 | Just call "move_and_stair_step()" instead of "move_and_slide()". 5 | 6 | Only tested with cylinder colliders. Works best with "0.01" collider margin. 7 | 8 | There are a couple signals you can connect to: 9 | - on_stair_step (any step, up or down) 10 | - on_stair_step_down 11 | - on_stair_step_up 12 | 13 | Example usage: 14 | ```gdscript 15 | 16 | extends StairsCharacter3D 17 | 18 | 19 | const SPEED = 5.0 20 | const JUMP_VELOCITY = 4.5 21 | 22 | # Get the gravity from the project settings to be synced with RigidBody nodes. 23 | var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity") 24 | 25 | 26 | func _physics_process(delta: float) -> void: 27 | # Add the gravity. 28 | if not is_on_floor(): 29 | velocity.y -= gravity * delta 30 | 31 | # Handle jump. 32 | if Input.is_action_just_pressed("ui_accept") and is_on_floor(): 33 | velocity.y = JUMP_VELOCITY 34 | 35 | # Get the input direction and handle the movement/deceleration. 36 | # As good practice, you should replace UI actions with custom gameplay actions. 37 | var input_dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") 38 | var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() 39 | if direction: 40 | velocity.x = direction.x * SPEED 41 | velocity.z = direction.z * SPEED 42 | else: 43 | velocity.x = move_toward(velocity.x, 0, SPEED) 44 | velocity.z = move_toward(velocity.z, 0, SPEED) 45 | 46 | # call move_and_stair_step instead of default move_and_slide 47 | move_and_stair_step() 48 | ``` 49 | 50 | # Note 51 | This plugin is basically just [Andricraft's GDScript Stairs Character](https://github.com/Andicraft/stairs-character) translated into C++. 52 | I made it as a learning exercise for GDExtension. 53 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 🛠️ Builds 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | build: 9 | runs-on: ${{ matrix.runner }} 10 | name: ${{ matrix.name }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - identifier: linux-debug-x86_64 16 | name: Linux Debug x86_64 17 | runner: ubuntu-22.04 18 | target: template_debug 19 | platform: linux 20 | arch: x86_64 21 | - identifier: linux-release-x86_64 22 | name: Linux Release x86_64 23 | runner: ubuntu-22.04 24 | target: template_release 25 | platform: linux 26 | arch: x86_64 27 | - identifier: windows-debug-x86_64 28 | name: Windows Debug x86_64 29 | runner: ubuntu-22.04 30 | target: template_debug 31 | platform: windows 32 | arch: x86_64 33 | - identifier: windows-release-x86_64 34 | name: Windows Release x86_64 35 | runner: ubuntu-22.04 36 | target: template_release 37 | platform: windows 38 | arch: x86_64 39 | - identifier: android-debug-arm64v8 40 | name: Android Debug arm64v8 41 | runner: ubuntu-22.04 42 | target: template_debug 43 | platform: android 44 | arch: arm64v8 45 | - identifier: android-release-arm64v8 46 | name: Android Release arm64v8 47 | runner: ubuntu-22.04 48 | target: template_release 49 | platform: android 50 | arch: arm64v8 51 | steps: 52 | - name: (Windows) Install mingw64 53 | if: ${{ startsWith(matrix.identifier, 'windows-') }} 54 | shell: sh 55 | run: | 56 | sudo apt-get install mingw-w64 57 | sudo update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix 58 | sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix 59 | - name: (Android) Install JDK 17 60 | if: ${{ startsWith(matrix.identifier, 'android-') }} 61 | uses: actions/setup-java@v3 62 | with: 63 | java-version: 17 64 | distribution: temurin 65 | - name: (Android) Install Android SDK 66 | if: ${{ startsWith(matrix.identifier, 'android-') }} 67 | uses: android-actions/setup-android@v3 68 | - name: (Android) Install NDK r23c 69 | if: ${{ matrix.platform == 'android' }} 70 | uses: nttld/setup-ndk@v1 71 | with: 72 | ndk-version: r23c 73 | link-to-sdk: true 74 | - name: Install Python 75 | uses: actions/setup-python@v2 76 | - name: Install SCons 77 | shell: bash 78 | run: | 79 | python -c "import sys; print(sys.version)" 80 | python -m pip install scons 81 | scons --version 82 | - name: Checkout project 83 | uses: actions/checkout@v4 84 | with: 85 | submodules: recursive 86 | fetch-depth: 0 87 | - name: Set up SCons cache 88 | uses: actions/cache@v3 89 | with: 90 | path: | 91 | ${{ github.workspace }}/.scons-cache/ 92 | ${{ github.workspace }}/**/.sconsign.dblite 93 | ${{ github.workspace }}/godot-cpp/gen/ 94 | key: ${{ matrix.identifier }}-${{ github.ref }}-${{ github.sha }} 95 | restore-keys: | 96 | ${{ matrix.identifier }}-${{ github.ref }}-${{ github.sha }} 97 | ${{ matrix.identifier }}-${{ github.ref }}- 98 | ${{ matrix.identifier }}- 99 | - name: Compile extension 100 | shell: bash 101 | env: 102 | SCONS_CACHE: '${{ github.workspace }}/.scons-cache/' 103 | SCONS_CACHE_LIMIT: 4096 104 | run: | 105 | scons target='${{ matrix.target }}' platform='${{ matrix.platform }}' arch='${{ matrix.arch }}' -j2 106 | ls -l project/addons/*/bin/ 107 | # TODO: Replace '--no-clobber' with '--update=none' in ubuntu 24.04 108 | # TODO: Workaround artifact folder structure: https://github.com/actions/upload-artifact/issues/174 109 | - name: Copy README.md and LICENSE to addon and make a new directory for artifacts 110 | shell: bash 111 | run: | 112 | for addon in ${{ github.workspace }}/project/addons/*/; do 113 | cp --no-clobber '${{ github.workspace }}/README.md' '${{ github.workspace }}/LICENSE' "$addon" 114 | done 115 | mkdir --parents ${{ github.workspace }}/github-artifact/${{ github.event.repository.name }}-${{ github.sha }}/ 116 | mv ${{ github.workspace }}/project/addons/ ${{ github.workspace }}/github-artifact/${{ github.event.repository.name }}-${{ github.sha }}/ 117 | - name: Upload artifact 118 | uses: actions/upload-artifact@v4 119 | with: 120 | name: ${{ matrix.identifier }} 121 | path: | 122 | ${{ github.workspace }}/github-artifact 123 | merge: 124 | runs-on: ubuntu-22.04 125 | needs: build 126 | steps: 127 | # TODO: Simplify generating artifact names. See: https://github.com/orgs/community/discussions/26686 128 | - name: Merge artifacts 129 | uses: actions/upload-artifact/merge@v4 130 | with: 131 | name: ${{ github.event.repository.name }}-${{ github.event_name == 'pull_request' && format('pr{0}', github.event.number) || github.ref_name }} 132 | delete-merged: true 133 | -------------------------------------------------------------------------------- /src/StairsCharacter.cpp: -------------------------------------------------------------------------------- 1 | #include "StairsCharacter.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace godot; 10 | 11 | void StairsCharacter3D::_bind_methods() 12 | { 13 | ClassDB::bind_method(D_METHOD("get_collider"), &StairsCharacter3D::get_collider); 14 | ClassDB::bind_method(D_METHOD("set_collider", "collider"), &StairsCharacter3D::set_collider); 15 | ClassDB::add_property("StairsCharacter3D", PropertyInfo(Variant::NODE_PATH, "collider", PROPERTY_HINT_NODE_TYPE, "CollisionShape3D"), "set_collider", "get_collider"); 16 | 17 | ClassDB::bind_method(D_METHOD("get_step_height_up"), &StairsCharacter3D::get_step_height_up); 18 | ClassDB::bind_method(D_METHOD("set_step_height_up", "height"), &StairsCharacter3D::set_step_height_up); 19 | 20 | ClassDB::bind_method(D_METHOD("get_step_height_down"), &StairsCharacter3D::get_step_height_down); 21 | ClassDB::bind_method(D_METHOD("set_step_height_down", "height"), &StairsCharacter3D::set_step_height_down); 22 | 23 | ClassDB::add_property("StairsCharacter3D", PropertyInfo(Variant::FLOAT, "step_height_up", PROPERTY_HINT_RANGE, "0,5,0.05"), "set_step_height_up", "get_step_height_up"); 24 | ClassDB::add_property("StairsCharacter3D", PropertyInfo(Variant::FLOAT, "step_height_down", PROPERTY_HINT_RANGE, "0,5,0.05"), "set_step_height_down", "get_step_height_down"); 25 | 26 | ClassDB::bind_method(D_METHOD("move_and_stair_step"), &StairsCharacter3D::move_and_stair_step); 27 | 28 | ADD_SIGNAL(MethodInfo("on_stair_step_up")); 29 | ADD_SIGNAL(MethodInfo("on_stair_step_down")); 30 | ADD_SIGNAL(MethodInfo("on_stair_step")); 31 | } 32 | 33 | void StairsCharacter3D::_ready() 34 | { 35 | 36 | Node *col = get_node_or_null(collider); 37 | if (col == nullptr) { 38 | UtilityFunctions::push_error("Collider is null"); 39 | } 40 | } 41 | 42 | void StairsCharacter3D::reset_grounded() 43 | { 44 | was_grounded = grounded; 45 | grounded = is_on_floor(); 46 | desired_velocity = Vector3(0,0,0); 47 | } 48 | 49 | 50 | 51 | void StairsCharacter3D::move_and_stair_step() 52 | { 53 | reset_grounded(); 54 | set_desired_velocity(get_velocity().normalized()); 55 | stair_step_up(); 56 | move_and_slide(); 57 | stair_step_down(); 58 | } 59 | 60 | void StairsCharacter3D::stair_step_up() 61 | { 62 | if (grounded == false) { 63 | return; 64 | } 65 | 66 | Vector3 horizontal_velocity = horizontal * get_velocity(); 67 | Vector3 testing_velocity = desired_velocity; 68 | if (horizontal_velocity != Vector3(0,0,0)) { 69 | testing_velocity = horizontal_velocity; 70 | } 71 | 72 | // Not moving or attempting to move, skip stair check 73 | if (testing_velocity == Vector3(0,0,0)) { 74 | return; 75 | } 76 | 77 | Ref result = memnew(PhysicsTestMotionResult3D); 78 | Ref params = memnew(PhysicsTestMotionParameters3D); 79 | 80 | params->set_margin(0.01); 81 | 82 | // This variable gets reused for all the following checks 83 | Transform3D motion_transform = get_global_transform(); 84 | 85 | Vector3 distance = testing_velocity * get_physics_process_delta_time(); 86 | params->set_from(motion_transform); 87 | params->set_motion(distance); 88 | 89 | // No stair step to do, we didn't hit any walls 90 | if (PhysicsServer3D::get_singleton()->body_test_motion(get_rid(), params, result) == false) { 91 | return; 92 | } 93 | 94 | // Move to collision 95 | Vector3 remainder = result->get_remainder(); 96 | motion_transform = motion_transform.translated(result->get_travel()); 97 | 98 | // Raise up to ceiling - can't walk on steps if there's a low ceiling 99 | Vector3 step_up = step_height_up * Vector3(0,1,0); 100 | 101 | 102 | params->set_from(motion_transform); 103 | params->set_motion(step_up); 104 | PhysicsServer3D::get_singleton()->body_test_motion(get_rid(), params, result); 105 | 106 | 107 | // GetTravel will be full length if we didn't hit anything 108 | motion_transform = motion_transform.translated(result->get_travel()); 109 | float step_up_distance = result->get_travel().length(); 110 | 111 | // Move forward remaining distance 112 | params->set_from(motion_transform); 113 | params->set_motion(remainder); 114 | PhysicsServer3D::get_singleton()->body_test_motion(get_rid(), params, result); 115 | motion_transform = motion_transform.translated(result->get_travel()); 116 | 117 | // And set the collider back down again 118 | params->set_from(motion_transform); 119 | // But no further than how far we stepped up 120 | params->set_motion(Vector3(0, -1, 0) * step_up_distance); 121 | 122 | // Don't bother with the rest if we're not actually gonna land back down on something 123 | if (PhysicsServer3D::get_singleton()->body_test_motion(get_rid(), params, result) == false) { 124 | return; 125 | } 126 | 127 | motion_transform = motion_transform.translated(result->get_travel()); 128 | 129 | Vector3 surface_normal = result->get_collision_normal(); 130 | 131 | // Can't stand on the thing we're trying to step on anyway 132 | if (surface_normal.angle_to(Vector3(0, 1, 0)) > get_floor_max_angle()) { 133 | return; 134 | } 135 | 136 | // Move player to match the step height we just found 137 | set_global_position(Vector3(get_global_position().x, motion_transform.origin.y, get_global_position().z)); 138 | emit_signal("on_stair_step"); 139 | emit_signal("on_stair_step_up"); 140 | } 141 | 142 | void StairsCharacter3D::stair_step_down() 143 | { 144 | if (was_grounded == false || get_velocity().y >= 0) 145 | { 146 | return; 147 | } 148 | 149 | Ref result = memnew(PhysicsTestMotionResult3D); 150 | Ref params = memnew(PhysicsTestMotionParameters3D); 151 | 152 | params->set_from(get_global_transform()); 153 | params->set_motion(Vector3(0,-1,0) * step_height_down); 154 | params->set_margin(0.01); 155 | 156 | if (PhysicsServer3D::get_singleton()->body_test_motion(get_rid(), params, result) == false) { 157 | return; 158 | } 159 | 160 | set_global_transform(get_global_transform().translated(result->get_travel())); 161 | apply_floor_snap(); 162 | emit_signal("on_stair_step"); 163 | emit_signal("on_stair_step_down"); 164 | } 165 | 166 | void StairsCharacter3D::set_collider(const NodePath &p_collider) 167 | { 168 | collider = p_collider; 169 | } 170 | 171 | NodePath StairsCharacter3D::get_collider() const 172 | { 173 | return collider; 174 | } --------------------------------------------------------------------------------