Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions doc/classes/CharacterBody3D.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@
Returns the number of times the body collided and changed direction during the last call to [method move_and_slide].
</description>
</method>
<method name="get_visual_position" qualifiers="const">
<return type="Vector3" />
<description>
Returns a smoothed position for visual elements like cameras. When [member step_smooth_enabled] is [code]true[/code], this position interpolates the vertical component during step-up and step-down events, preventing jarring camera movement when traversing stairs. The horizontal components match the actual physics position.
Use this for camera positioning instead of [member Node3D.global_position] when step handling is enabled.
Comment on lines +92 to +93
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Returns a smoothed position for visual elements like cameras. When [member step_smooth_enabled] is [code]true[/code], this position interpolates the vertical component during step-up and step-down events, preventing jarring camera movement when traversing stairs. The horizontal components match the actual physics position.
Use this for camera positioning instead of [member Node3D.global_position] when step handling is enabled.
Returns a smoothed position for visual elements like cameras. When [member step_smooth_enabled] is [code]true[/code], this position interpolates the vertical component during step-up and step-down events, preventing jarring camera movement when traversing stairs. The horizontal components match the actual physics position.
Use this for camera positioning instead of [member Node3D.global_position] when step handling is enabled.
[codeblocks]
[gdscript]
var old_visual_position = get_visual_position()
var new_visual_position = get_visual_position()
func _process(_delta: float) -> void:
# Apply step smoothing with manual physics interpolation.
# The Camera3D node is marked as top-level and has physics
# interpolation set to Off in the inspector.
$Camera3D.global_position = old_visual_position.lerp(new_visual_position, Engine.get_physics_interpolation_fraction())
func _physics_process(delta: float) -> void:
old_visual_position = get_visual_position()
# [...]
move_and_slide()
new_visual_position = get_visual_position()
[/gdscript]
[/codeblocks]

</description>
</method>
<method name="get_wall_normal" qualifiers="const">
<return type="Vector3" />
<description>
Expand Down Expand Up @@ -183,6 +190,19 @@
<member name="slide_on_ceiling" type="bool" setter="set_slide_on_ceiling_enabled" getter="is_slide_on_ceiling_enabled" default="true">
If [code]true[/code], during a jump against the ceiling, the body will slide, if [code]false[/code] it will be stopped and will fall vertically.
</member>
<member name="step_enabled" type="bool" setter="set_step_enabled" getter="is_step_enabled" default="false">
If [code]true[/code], the body will automatically step up onto obstacles shorter than [member step_height] when calling [method move_and_slide]. Uses an up-forward-down trace algorithm similar to classic FPS engines. Only works when [member motion_mode] is [constant MOTION_MODE_GROUNDED].
[b]Note:[/b] For best results, use a [CylinderShape3D] collider. [CapsuleShape3D] colliders have rounded bottoms that can cause step detection issues.
</member>
<member name="step_height" type="float" setter="set_step_height" getter="get_step_height" default="0.3">
Maximum height of obstacles the body can step onto when [member step_enabled] is [code]true[/code]. Obstacles taller than this will block movement and cause sliding.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Maximum height of obstacles the body can step onto when [member step_enabled] is [code]true[/code]. Obstacles taller than this will block movement and cause sliding.
Maximum height of obstacles the body can step onto when [member step_enabled] is [code]true[/code]. Obstacles taller than this will block movement and cause sliding.
[b]Note:[/b] Set [member floor_snap_height] to the same value as [member step_height] to also enable "step down" behavior, with the same camera smoothing.

</member>
<member name="step_smooth_enabled" type="bool" setter="set_step_smooth_enabled" getter="is_step_smooth_enabled" default="true">
If [code]true[/code], the [method get_visual_position] method returns a smoothed position that interpolates during step events. This prevents jarring camera movement when traversing stairs.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If [code]true[/code], the [method get_visual_position] method returns a smoothed position that interpolates during step events. This prevents jarring camera movement when traversing stairs.
If [code]true[/code], the [method get_visual_position] method returns a smoothed position that interpolates during step events. This prevents jarring camera movement when traversing stairs.
[b]Note:[/b] Step smoothing applies on any height change while on the floor, which means [member floor_snap_height] will also result in step smoothing.

</member>
<member name="step_smooth_speed" type="float" setter="set_step_smooth_speed" getter="get_step_smooth_speed" default="10.0">
Controls how quickly [method get_visual_position] catches up to the actual position during step events. Higher values result in faster, snappier camera response. This is a rate factor, not meters per second.
</member>
<member name="up_direction" type="Vector3" setter="set_up_direction" getter="get_up_direction" default="Vector3(0, 1, 0)">
Vector pointing upwards, used to determine what is a wall and what is a floor (or a ceiling) when calling [method move_and_slide]. Defaults to [constant Vector3.UP]. As the vector will be normalized it can't be equal to [constant Vector3.ZERO], if you want all collisions to be reported as walls, consider using [constant MOTION_MODE_FLOATING] as [member motion_mode].
</member>
Expand Down
1 change: 1 addition & 0 deletions modules/gdscript/gdscript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2614,6 +2614,7 @@ Vector<String> GDScriptLanguage::get_reserved_words() const {
"enum",
"extends",
"func",
"implements",
"namespace", // Reserved for potential future use.
"signal",
"static",
Expand Down
1 change: 1 addition & 0 deletions modules/gdscript/gdscript.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class GDScript : public Script {
HashMap<StringName, Variant> constants;
HashMap<StringName, GDScriptFunction *> member_functions;
HashMap<StringName, Ref<GDScript>> subclasses;
Vector<Ref<GDScript>> implemented_traits; // Traits declared via `implements`, used by `is`/`as` at runtime.
HashMap<StringName, MethodInfo> _signals;
Dictionary rpc_config;

Expand Down
112 changes: 111 additions & 1 deletion modules/gdscript/gdscript_analyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,11 @@ Error GDScriptAnalyzer::resolve_class_inheritance(GDScriptParser::ClassNode *p_c
E->apply(parser, p_class, p_class->outer);
}

// Resolve implemented trait types now so type-compatibility checks can see them later.
for (GDScriptParser::TypeNode *trait_type_node : p_class->implemented_traits) {
resolve_datatype(trait_type_node);
}

parser->current_class = previous_class;

return OK;
Expand Down Expand Up @@ -1567,9 +1572,93 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co
}
}

// Verify that a concrete class fulfills every trait it claims to implement.
// Abstract classes are allowed to defer this to their concrete subclasses.
if (!p_class->is_abstract && p_class->implements_used) {
check_trait_implementations(p_class);
}

parser->current_class = previous_class;
}

void GDScriptAnalyzer::check_trait_implementations(GDScriptParser::ClassNode *p_class) {
const String class_name = p_class->identifier == nullptr ? p_class->fqcn.get_file() : String(p_class->identifier->name);

GDScriptParser::DataType self_type = p_class->get_datatype();
self_type.is_meta_type = false;

for (GDScriptParser::TypeNode *trait_type_node : p_class->implemented_traits) {
GDScriptParser::DataType trait_type = type_from_metatype(resolve_datatype(trait_type_node));

if (trait_type.kind != GDScriptParser::DataType::CLASS || trait_type.class_type == nullptr || !trait_type.class_type->is_trait) {
push_error(vformat(R"(Type "%s" used in "implements" is not a trait.)", trait_type.to_string()), trait_type_node);
continue;
}

GDScriptParser::ClassNode *trait_class = trait_type.class_type;

// Make sure the trait's members and their signatures are resolved.
resolve_class_interface(trait_class, p_class);

const String trait_name = trait_class->identifier == nullptr ? trait_class->fqcn.get_file() : String(trait_class->identifier->name);

for (const GDScriptParser::ClassNode::Member &member : trait_class->members) {
if (member.type != GDScriptParser::ClassNode::Member::FUNCTION) {
continue;
}
const StringName &function_name = member.function->identifier->name;

// Expected signature, as declared by the trait.
GDScriptParser::DataType trait_return;
List<GDScriptParser::DataType> trait_params;
int trait_default_count = 0;
BitField<MethodFlags> trait_flags = {};
get_function_signature(p_class, false, trait_type, function_name, trait_return, trait_params, trait_default_count, trait_flags);

// Actual signature, as found in the class or its base chain.
GDScriptParser::DataType impl_return;
List<GDScriptParser::DataType> impl_params;
int impl_default_count = 0;
BitField<MethodFlags> impl_flags = {};
if (!get_function_signature(p_class, false, self_type, function_name, impl_return, impl_params, impl_default_count, impl_flags) || impl_flags.has_flag(METHOD_FLAG_VIRTUAL_REQUIRED)) {
push_error(vformat(R"*(Class "%s" must implement "%s.%s()".)*", class_name, trait_name, function_name), p_class);
continue;
}

bool valid = impl_flags.has_flag(METHOD_FLAG_STATIC) == trait_flags.has_flag(METHOD_FLAG_STATIC);

// `[impl_min..impl_max]` must include `[trait_min..trait_max]`.
const int trait_min_argc = trait_params.size() - trait_default_count;
const int trait_max_argc = trait_flags.has_flag(METHOD_FLAG_VARARG) ? INT_MAX : trait_params.size();
const int impl_min_argc = impl_params.size() - impl_default_count;
const int impl_max_argc = impl_flags.has_flag(METHOD_FLAG_VARARG) ? INT_MAX : impl_params.size();
valid = valid && impl_min_argc <= trait_min_argc && trait_max_argc <= impl_max_argc;

// Return type must be covariant.
if (valid && !trait_return.is_variant()) {
valid = is_type_compatible(trait_return, impl_return);
}

// Parameter types must be contravariant.
const List<GDScriptParser::DataType>::Element *trait_it = trait_params.front();
const List<GDScriptParser::DataType>::Element *impl_it = impl_params.front();
while (valid && trait_it != nullptr && impl_it != nullptr) {
const GDScriptParser::DataType &trait_par = trait_it->get();
const GDScriptParser::DataType &impl_par = impl_it->get();
if (!trait_par.is_variant() && !impl_par.is_variant()) {
valid = is_type_compatible(impl_par, trait_par);
}
trait_it = trait_it->next();
impl_it = impl_it->next();
}

if (!valid) {
push_error(vformat(R"*(The signature of "%s.%s()" does not match the signature declared by trait "%s".)*", class_name, function_name, trait_name), p_class);
}
}
}
}

void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, bool p_recursive) {
resolve_class_body(p_class);

Expand Down Expand Up @@ -3814,6 +3903,13 @@ void GDScriptAnalyzer::reduce_cast(GDScriptParser::CastNode *p_cast) {
valid = is_type_compatible(cast_type, op_type) || is_type_compatible(op_type, cast_type);
}

// A trait can be implemented by otherwise-unrelated types, so `as SomeTrait` is a
// legitimate runtime-checked cast even when the static types are unrelated.
if (!valid && cast_type.kind == GDScriptParser::DataType::CLASS && cast_type.class_type != nullptr && cast_type.class_type->is_trait) {
valid = true;
mark_node_unsafe(p_cast);
}

if (!valid) {
push_error(vformat(R"(Invalid cast. Cannot convert from "%s" to "%s".)", op_type.to_string(), cast_type.to_string()), p_cast->cast_type);
}
Expand Down Expand Up @@ -5210,7 +5306,11 @@ void GDScriptAnalyzer::reduce_type_test(GDScriptParser::TypeTestNode *p_type_tes
return;
}

if (!is_type_compatible(test_type, operand_type) && !is_type_compatible(operand_type, test_type)) {
// A trait can be implemented by otherwise-unrelated types, so `is SomeTrait` is always a
// legitimate runtime check and must not be rejected based on the operand's static type.
const bool test_is_trait = test_type.kind == GDScriptParser::DataType::CLASS && test_type.class_type != nullptr && test_type.class_type->is_trait;

if (!test_is_trait && !is_type_compatible(test_type, operand_type) && !is_type_compatible(operand_type, test_type)) {
if (operand_type.is_hard_type()) {
push_error(vformat(R"(Expression is of type "%s" so it can't be of type "%s".)", operand_type.to_string(), test_type.to_string()), p_type_test->operand);
} else {
Expand Down Expand Up @@ -6406,6 +6506,16 @@ bool GDScriptAnalyzer::check_type_compatibility(const GDScriptParser::DataType &
if (src_class == p_target.class_type || src_class->fqcn == p_target.class_type->fqcn) {
return true;
}
// A class is compatible with a trait it (or a base class) implements.
if (p_target.class_type->is_trait) {
for (const GDScriptParser::TypeNode *trait_type_node : src_class->implemented_traits) {
const GDScriptParser::DataType impl_trait = trait_type_node->get_datatype();
if (impl_trait.kind == GDScriptParser::DataType::CLASS && impl_trait.class_type != nullptr &&
(impl_trait.class_type == p_target.class_type || impl_trait.class_type->fqcn == p_target.class_type->fqcn)) {
return true;
}
}
}
src_class = src_class->base_type.class_type;
}
return false;
Expand Down
1 change: 1 addition & 0 deletions modules/gdscript/gdscript_analyzer.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class GDScriptAnalyzer {
void resolve_class_interface(GDScriptParser::ClassNode *p_class, bool p_recursive);
void resolve_class_body(GDScriptParser::ClassNode *p_class, const GDScriptParser::Node *p_source = nullptr);
void resolve_class_body(GDScriptParser::ClassNode *p_class, bool p_recursive);
void check_trait_implementations(GDScriptParser::ClassNode *p_class);
void resolve_function_signature(GDScriptParser::FunctionNode *p_function, const GDScriptParser::Node *p_source = nullptr, bool p_is_lambda = false);
void resolve_function_body(GDScriptParser::FunctionNode *p_function, bool p_is_lambda = false);
void resolve_node(GDScriptParser::Node *p_node, bool p_is_root = true);
Expand Down
31 changes: 28 additions & 3 deletions modules/gdscript/gdscript_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,21 @@ void GDScriptCompiler::_set_error(const String &p_error, const GDScriptParser::N
}
}

GDScriptDataType GDScriptCompiler::_gdtype_from_datatype(const GDScriptParser::DataType &p_datatype, GDScript *p_owner, bool p_handle_metatype) {
GDScriptDataType GDScriptCompiler::_gdtype_from_datatype(const GDScriptParser::DataType &p_datatype, GDScript *p_owner, bool p_handle_metatype, bool p_lower_traits) {
if (!p_datatype.is_set() || !p_datatype.is_hard_type() || p_datatype.is_coroutine) {
return GDScriptDataType();
}

// Traits are a compile-time-only contract with no runtime type identity. Their
// `implements` relationship is verified by the analyzer, but the VM knows nothing
// about it, so a trait-typed value must be left untyped at runtime to avoid
// spurious type checks (e.g. when passed to a trait-typed parameter). The
// exceptions are `is`/`as` and the per-class trait list, which need the real
// trait script type to perform the runtime check; those pass `p_lower_traits = false`.
if (p_lower_traits && !(p_handle_metatype && p_datatype.is_meta_type) && p_datatype.kind == GDScriptParser::DataType::CLASS && p_datatype.class_type != nullptr && p_datatype.class_type->is_trait) {
return GDScriptDataType();
}

GDScriptDataType result;

switch (p_datatype.kind) {
Expand Down Expand Up @@ -580,7 +590,8 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
} break;
case GDScriptParser::Node::CAST: {
const GDScriptParser::CastNode *cn = static_cast<const GDScriptParser::CastNode *>(p_expression);
GDScriptDataType cast_type = _gdtype_from_datatype(cn->get_datatype(), codegen.script, false);
// `p_lower_traits = false`: a cast to a trait needs the real trait script type so the VM can check `implements`.
GDScriptDataType cast_type = _gdtype_from_datatype(cn->get_datatype(), codegen.script, false, false);

GDScriptCodeGenerator::Address result;
if (cast_type.has_type()) {
Expand Down Expand Up @@ -962,7 +973,8 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
GDScriptCodeGenerator::Address result = codegen.add_temporary(_gdtype_from_datatype(type_test->get_datatype(), codegen.script));

GDScriptCodeGenerator::Address operand = _parse_expression(codegen, r_error, type_test->operand);
GDScriptDataType test_type = _gdtype_from_datatype(type_test->test_datatype, codegen.script, false);
// `p_lower_traits = false`: `is SomeTrait` needs the real trait script type so the VM can check `implements`.
GDScriptDataType test_type = _gdtype_from_datatype(type_test->test_datatype, codegen.script, false, false);
if (r_error) {
return GDScriptCodeGenerator::Address();
}
Expand Down Expand Up @@ -3014,6 +3026,19 @@ Error GDScriptCompiler::_prepare_compilation(GDScript *p_script, const GDScriptP
}

Error GDScriptCompiler::_compile_class(GDScript *p_script, const GDScriptParser::ClassNode *p_class, bool p_keep_state) {
// Record the traits this class implements so `is`/`as` can verify them at runtime.
p_script->implemented_traits.clear();
for (GDScriptParser::TypeNode *trait_node : p_class->implemented_traits) {
// `p_lower_traits = false` keeps the real trait script type instead of lowering it to untyped.
GDScriptDataType trait_type = _gdtype_from_datatype(trait_node->get_datatype(), p_script, false, false);
if (trait_type.kind == GDScriptDataType::GDSCRIPT || trait_type.kind == GDScriptDataType::SCRIPT) {
Ref<GDScript> trait_script = Object::cast_to<GDScript>(trait_type.script_type);
if (trait_script.is_valid()) {
p_script->implemented_traits.push_back(trait_script);
}
}
}

// Compile member functions, getters, and setters.
for (int i = 0; i < p_class->members.size(); i++) {
const GDScriptParser::ClassNode::Member &member = p_class->members[i];
Expand Down
2 changes: 1 addition & 1 deletion modules/gdscript/gdscript_compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class GDScriptCompiler {

void _set_error(const String &p_error, const GDScriptParser::Node *p_node);

GDScriptDataType _gdtype_from_datatype(const GDScriptParser::DataType &p_datatype, GDScript *p_owner, bool p_handle_metatype = true);
GDScriptDataType _gdtype_from_datatype(const GDScriptParser::DataType &p_datatype, GDScript *p_owner, bool p_handle_metatype = true, bool p_lower_traits = true);

GDScriptCodeGenerator::Address _parse_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::ExpressionNode *p_expression, bool p_root = false, bool p_initializer = false);
GDScriptCodeGenerator::Address _parse_match_pattern(CodeGen &codegen, Error &r_error, const GDScriptParser::PatternNode *p_pattern, const GDScriptCodeGenerator::Address &p_value_addr, const GDScriptCodeGenerator::Address &p_type_addr, const GDScriptCodeGenerator::Address &p_previous_test, bool p_is_first, bool p_is_nested);
Expand Down
Loading
Loading