学习演示项目dodge_the_creeps,发现里面多了一个Path2D与PathFollow2D
研究GDScript代码发现,它主要用于随机生成Mob
var mob_spawn_location = get_node(^"MobPath/MobSpawnLocation")mob_spawn_location.progress = randi()# Set the mob's direction perpendicular to the path direction.var direction = mob_spawn_location.rotation + PI / 2# Set the mob's position to a random location.mob.position = mob_spawn_location.position# Add some randomness to the direction.direction += randf_range(-PI / 4, PI / 4)mob.rotation = direction# Choose the velocity for the mob.var velocity = Vector2(randf_range(150.0, 250.0), 0.0)mob.linear_velocity = velocity.rotated(direction)
这个有这么大的作用,不明觉厉
但不知道如何下手
查看源码,有编辑器及类源码
先从应用角度,到B站上找找有没有视频,结果发现这个
Godot塔防游戏 - 01 -核心路径制作 Path2D_哔哩哔哩_bilibili
看了之后,就知道使用方法了:
- 添加Path2D
- 在编辑器中设置路径各关键点,形成路径
- 在Path2D下增加PathFollow2D
这就OK了。剩下的就是使用
所谓使用,输入为PathFollow2D的progress,输出为路径上的点信息(position, rotation...),然后用户再根据这些信息去确定相应的属性
比如演示项目中,Path2D定制了一个外框路径(左上角 > 右上角 > 右下角 > 左下角 > 左上角),在生成MOB时,随机指定其下的PathFollow2D的progress值为randi(),即为0 ~ 2^32 - 1的随机整数。因为路径是有长度的,本例中为2400,randi()值将按2400取模得到最终的随机值0 - 2399,当然也可以归一化,设置其progress_ratio值为0.0 - 1.0,意思一样。
查看源码,set_progress的逻辑不只是取模,还有限制范围。即PathFollow2D还有一个Loop属性,如果Loop为真,才会取模,为false时,会直接限制在路径长度范围内 progress = CLAMP(progress, 0, path_length); 之后统一更新_update_transform
void PathFollow2D::set_progress(real_t p_progress) {ERR_FAIL_COND(!isfinite(p_progress));progress = p_progress;if (path) {if (path->get_curve().is_valid()) {real_t path_length = path->get_curve()->get_baked_length();if (loop && path_length) {progress = Math::fposmod(progress, path_length);if (!Math::is_zero_approx(p_progress) && Math::is_zero_approx(progress)) {progress = path_length;}} else {progress = CLAMP(progress, 0, path_length);}}_update_transform();}
}void PathFollow2D::_update_transform() {if (!path) {return;}Ref<Curve2D> c = path->get_curve();if (!c.is_valid()) {return;}real_t path_length = c->get_baked_length();if (path_length == 0) {return;}if (rotates) {Transform2D xform = c->sample_baked_with_rotation(progress, cubic);xform.translate_local(v_offset, h_offset);set_rotation(xform[1].angle());set_position(xform[2]);} else {Vector2 pos = c->sample_baked(progress, cubic);pos.x += h_offset;pos.y += v_offset;set_position(pos);}
}
从PathFollow2D代码来看,它派生于Node2D,所以具备transform属性:Position、Rotation、Scale、Skew,对于路径上的点使用而言,这些信息就足够了,能够确定这些点的位置、方向,其实就是一个矢量
Loop属性值的含义前面已明确,Rotates、Cubic、H Offsets、V Offsets都是在_update_transform中起作用,具体算法可以不深究。但lookahead没找到具体用处,感觉影响不大。
class PathFollow2D : public Node2D {GDCLASS(PathFollow2D, Node2D);public:
private:Path2D *path = nullptr;real_t progress = 0.0;Timer *update_timer = nullptr;real_t h_offset = 0.0;real_t v_offset = 0.0;real_t lookahead = 4.0;bool cubic = true;bool loop = true;bool rotates = true;void _update_transform();protected:void _validate_property(PropertyInfo &p_property) const;void _notification(int p_what);static void _bind_methods();public:void path_changed();void set_progress(real_t p_progress);real_t get_progress() const;void set_h_offset(real_t p_h_offset);real_t get_h_offset() const;void set_v_offset(real_t p_v_offset);real_t get_v_offset() const;void set_progress_ratio(real_t p_ratio);real_t get_progress_ratio() const;void set_lookahead(real_t p_lookahead);real_t get_lookahead() const;void set_loop(bool p_loop);bool has_loop() const;void set_rotates(bool p_rotates);bool is_rotating() const;void set_cubic_interpolation(bool p_enable);bool get_cubic_interpolation() const;PackedStringArray get_configuration_warnings() const override;PathFollow2D() {}
};void PathFollow2D::path_changed() {if (update_timer && !update_timer->is_stopped()) {update_timer->start();} else {_update_transform();}
}void PathFollow2D::_update_transform() {if (!path) {return;}Ref<Curve2D> c = path->get_curve();if (!c.is_valid()) {return;}real_t path_length = c->get_baked_length();if (path_length == 0) {return;}if (rotates) {Transform2D xform = c->sample_baked_with_rotation(progress, cubic);xform.translate_local(v_offset, h_offset);set_rotation(xform[1].angle());set_position(xform[2]);} else {Vector2 pos = c->sample_baked(progress, cubic);pos.x += h_offset;pos.y += v_offset;set_position(pos);}
}void PathFollow2D::_notification(int p_what) {switch (p_what) {case NOTIFICATION_READY: {if (Engine::get_singleton()->is_editor_hint()) {update_timer = memnew(Timer);update_timer->set_wait_time(0.2);update_timer->set_one_shot(true);update_timer->connect("timeout", callable_mp(this, &PathFollow2D::_update_transform));add_child(update_timer, false, Node::INTERNAL_MODE_BACK);}} break;case NOTIFICATION_ENTER_TREE: {path = Object::cast_to<Path2D>(get_parent());if (path) {_update_transform();}} break;case NOTIFICATION_EXIT_TREE: {path = nullptr;} break;}
}void PathFollow2D::set_cubic_interpolation(bool p_enable) {cubic = p_enable;
}bool PathFollow2D::get_cubic_interpolation() const {return cubic;
}void PathFollow2D::_validate_property(PropertyInfo &p_property) const {if (p_property.name == "offset") {real_t max = 10000.0;if (path && path->get_curve().is_valid()) {max = path->get_curve()->get_baked_length();}p_property.hint_string = "0," + rtos(max) + ",0.01,or_less,or_greater";}
}PackedStringArray PathFollow2D::get_configuration_warnings() const {PackedStringArray warnings = Node::get_configuration_warnings();if (is_visible_in_tree() && is_inside_tree()) {if (!Object::cast_to<Path2D>(get_parent())) {warnings.push_back(RTR("PathFollow2D only works when set as a child of a Path2D node."));}}return warnings;
}void PathFollow2D::_bind_methods() {ClassDB::bind_method(D_METHOD("set_progress", "progress"), &PathFollow2D::set_progress);ClassDB::bind_method(D_METHOD("get_progress"), &PathFollow2D::get_progress);ClassDB::bind_method(D_METHOD("set_h_offset", "h_offset"), &PathFollow2D::set_h_offset);ClassDB::bind_method(D_METHOD("get_h_offset"), &PathFollow2D::get_h_offset);ClassDB::bind_method(D_METHOD("set_v_offset", "v_offset"), &PathFollow2D::set_v_offset);ClassDB::bind_method(D_METHOD("get_v_offset"), &PathFollow2D::get_v_offset);ClassDB::bind_method(D_METHOD("set_progress_ratio", "ratio"), &PathFollow2D::set_progress_ratio);ClassDB::bind_method(D_METHOD("get_progress_ratio"), &PathFollow2D::get_progress_ratio);ClassDB::bind_method(D_METHOD("set_rotates", "enable"), &PathFollow2D::set_rotates);ClassDB::bind_method(D_METHOD("is_rotating"), &PathFollow2D::is_rotating);ClassDB::bind_method(D_METHOD("set_cubic_interpolation", "enable"), &PathFollow2D::set_cubic_interpolation);ClassDB::bind_method(D_METHOD("get_cubic_interpolation"), &PathFollow2D::get_cubic_interpolation);ClassDB::bind_method(D_METHOD("set_loop", "loop"), &PathFollow2D::set_loop);ClassDB::bind_method(D_METHOD("has_loop"), &PathFollow2D::has_loop);ClassDB::bind_method(D_METHOD("set_lookahead", "lookahead"), &PathFollow2D::set_lookahead);ClassDB::bind_method(D_METHOD("get_lookahead"), &PathFollow2D::get_lookahead);ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "progress", PROPERTY_HINT_RANGE, "0,10000,0.01,or_less,or_greater,suffix:px"), "set_progress", "get_progress");ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "progress_ratio", PROPERTY_HINT_RANGE, "0,1,0.0001,or_less,or_greater", PROPERTY_USAGE_EDITOR), "set_progress_ratio", "get_progress_ratio");ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "h_offset"), "set_h_offset", "get_h_offset");ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "v_offset"), "set_v_offset", "get_v_offset");ADD_PROPERTY(PropertyInfo(Variant::BOOL, "rotates"), "set_rotates", "is_rotating");ADD_PROPERTY(PropertyInfo(Variant::BOOL, "cubic_interp"), "set_cubic_interpolation", "get_cubic_interpolation");ADD_PROPERTY(PropertyInfo(Variant::BOOL, "loop"), "set_loop", "has_loop");ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lookahead", PROPERTY_HINT_RANGE, "0.001,1024.0,0.001"), "set_lookahead", "get_lookahead");
}void PathFollow2D::set_progress(real_t p_progress) {ERR_FAIL_COND(!isfinite(p_progress));progress = p_progress;if (path) {if (path->get_curve().is_valid()) {real_t path_length = path->get_curve()->get_baked_length();if (loop && path_length) {progress = Math::fposmod(progress, path_length);if (!Math::is_zero_approx(p_progress) && Math::is_zero_approx(progress)) {progress = path_length;}} else {progress = CLAMP(progress, 0, path_length);}}_update_transform();}
}void PathFollow2D::set_h_offset(real_t p_h_offset) {h_offset = p_h_offset;if (path) {_update_transform();}
}real_t PathFollow2D::get_h_offset() const {return h_offset;
}void PathFollow2D::set_v_offset(real_t p_v_offset) {v_offset = p_v_offset;if (path) {_update_transform();}
}real_t PathFollow2D::get_v_offset() const {return v_offset;
}real_t PathFollow2D::get_progress() const {return progress;
}void PathFollow2D::set_progress_ratio(real_t p_ratio) {if (path && path->get_curve().is_valid() && path->get_curve()->get_baked_length()) {set_progress(p_ratio * path->get_curve()->get_baked_length());}
}real_t PathFollow2D::get_progress_ratio() const {if (path && path->get_curve().is_valid() && path->get_curve()->get_baked_length()) {return get_progress() / path->get_curve()->get_baked_length();} else {return 0;}
}void PathFollow2D::set_lookahead(real_t p_lookahead) {lookahead = p_lookahead;
}real_t PathFollow2D::get_lookahead() const {return lookahead;
}void PathFollow2D::set_rotates(bool p_rotates) {rotates = p_rotates;_update_transform();
}bool PathFollow2D::is_rotating() const {return rotates;
}void PathFollow2D::set_loop(bool p_loop) {loop = p_loop;
}bool PathFollow2D::has_loop() const {return loop;
}
从代码与用途来看,Path2D就没啥看头了,就负责提供一条曲线路径
class Path2D : public Node2D {GDCLASS(Path2D, Node2D);Ref<Curve2D> curve;void _curve_changed();protected:void _notification(int p_what);static void _bind_methods();public:
#ifdef TOOLS_ENABLEDvirtual Rect2 _edit_get_rect() const override;virtual bool _edit_use_rect() const override;virtual bool _edit_is_selected_on_click(const Point2 &p_point, double p_tolerance) const override;
#endifvoid set_curve(const Ref<Curve2D> &p_curve);Ref<Curve2D> get_curve() const;Path2D() {}
};#ifdef TOOLS_ENABLED
Rect2 Path2D::_edit_get_rect() const {if (!curve.is_valid() || curve->get_point_count() == 0) {return Rect2(0, 0, 0, 0);}Rect2 aabb = Rect2(curve->get_point_position(0), Vector2(0, 0));for (int i = 0; i < curve->get_point_count(); i++) {for (int j = 0; j <= 8; j++) {real_t frac = j / 8.0;Vector2 p = curve->sample(i, frac);aabb.expand_to(p);}}return aabb;
}bool Path2D::_edit_use_rect() const {return curve.is_valid() && curve->get_point_count() != 0;
}bool Path2D::_edit_is_selected_on_click(const Point2 &p_point, double p_tolerance) const {if (curve.is_null()) {return false;}for (int i = 0; i < curve->get_point_count(); i++) {Vector2 s[2];s[0] = curve->get_point_position(i);for (int j = 1; j <= 8; j++) {real_t frac = j / 8.0;s[1] = curve->sample(i, frac);Vector2 p = Geometry2D::get_closest_point_to_segment(p_point, s);if (p.distance_to(p_point) <= p_tolerance) {return true;}s[0] = s[1];}}return false;
}
#endifvoid Path2D::_notification(int p_what) {switch (p_what) {// Draw the curve if path debugging is enabled.case NOTIFICATION_DRAW: {if (!curve.is_valid()) {break;}if (!Engine::get_singleton()->is_editor_hint() && !get_tree()->is_debugging_paths_hint()) {return;}if (curve->get_point_count() < 2) {return;}#ifdef TOOLS_ENABLEDconst real_t line_width = get_tree()->get_debug_paths_width() * EDSCALE;
#elseconst real_t line_width = get_tree()->get_debug_paths_width();
#endifreal_t interval = 10;const real_t length = curve->get_baked_length();if (length > CMP_EPSILON) {const int sample_count = int(length / interval) + 2;interval = length / (sample_count - 1); // Recalculate real interval length.Vector<Transform2D> frames;frames.resize(sample_count);{Transform2D *w = frames.ptrw();for (int i = 0; i < sample_count; i++) {w[i] = curve->sample_baked_with_rotation(i * interval, false);}}const Transform2D *r = frames.ptr();// Draw curve segments{PackedVector2Array v2p;v2p.resize(sample_count);Vector2 *w = v2p.ptrw();for (int i = 0; i < sample_count; i++) {w[i] = r[i].get_origin();}draw_polyline(v2p, get_tree()->get_debug_paths_color(), line_width, false);}// Draw fish bones{PackedVector2Array v2p;v2p.resize(3);Vector2 *w = v2p.ptrw();for (int i = 0; i < sample_count; i++) {const Vector2 p = r[i].get_origin();const Vector2 side = r[i].columns[0];const Vector2 forward = r[i].columns[1];// Fish Bone.w[0] = p + (side - forward) * 5;w[1] = p;w[2] = p + (-side - forward) * 5;draw_polyline(v2p, get_tree()->get_debug_paths_color(), line_width * 0.5, false);}}}} break;}
}void Path2D::_curve_changed() {if (!is_inside_tree()) {return;}if (!Engine::get_singleton()->is_editor_hint() && !get_tree()->is_debugging_paths_hint()) {return;}queue_redraw();for (int i = 0; i < get_child_count(); i++) {PathFollow2D *follow = Object::cast_to<PathFollow2D>(get_child(i));if (follow) {follow->path_changed();}}
}void Path2D::set_curve(const Ref<Curve2D> &p_curve) {if (curve.is_valid()) {curve->disconnect("changed", callable_mp(this, &Path2D::_curve_changed));}curve = p_curve;if (curve.is_valid()) {curve->connect("changed", callable_mp(this, &Path2D::_curve_changed));}_curve_changed();
}Ref<Curve2D> Path2D::get_curve() const {return curve;
}void Path2D::_bind_methods() {ClassDB::bind_method(D_METHOD("set_curve", "curve"), &Path2D::set_curve);ClassDB::bind_method(D_METHOD("get_curve"), &Path2D::get_curve);ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve2D", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_EDITOR_INSTANTIATE_OBJECT), "set_curve", "get_curve");
}
当然,也不是啥用处没有,比如动态指定路径的时候,就可以设置一条Curve2D,然后赋给Path2D,后面就照此行事。
比如,该演示项目中,
var curve = Curve2D.new()
curve.add_point(Vector2i(100, 100))
curve.add_point(Vector2i(400, 600))
$MobPath.curve = curve
然后,玩家呆在右上角,这就是那些MOB的死角,玩家可以活到把用户送走