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
30 changes: 28 additions & 2 deletions core/config/engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,40 @@
#include "core/version.h"
#include "servers/rendering/rendering_device.h"

void Engine::_update_time_scale() {
_time_scale = _user_time_scale * _game_time_scale;
user_ips = MAX(1, ips * _user_time_scale);
max_user_physics_steps_per_frame = MAX(max_physics_steps_per_frame, max_physics_steps_per_frame * _user_time_scale);
}

void Engine::set_physics_ticks_per_second(int p_ips) {
ERR_FAIL_COND_MSG(p_ips <= 0, "Engine iterations per second must be greater than 0.");
ips = p_ips;
_update_time_scale();
}

int Engine::get_physics_ticks_per_second() const {
return ips;
}

int Engine::get_user_physics_ticks_per_second() const {
return user_ips;
}

void Engine::set_max_physics_steps_per_frame(int p_max_physics_steps) {
ERR_FAIL_COND_MSG(p_max_physics_steps <= 0, "Maximum number of physics steps per frame must be greater than 0.");
max_physics_steps_per_frame = p_max_physics_steps;
_update_time_scale();
}

int Engine::get_max_physics_steps_per_frame() const {
return max_physics_steps_per_frame;
}

int Engine::get_user_max_physics_steps_per_frame() const {
return max_user_physics_steps_per_frame;
}

void Engine::set_physics_jitter_fix(double p_threshold) {
if (p_threshold < 0) {
p_threshold = 0;
Expand Down Expand Up @@ -121,11 +137,21 @@ uint32_t Engine::get_frame_delay() const {
}

void Engine::set_time_scale(double p_scale) {
_time_scale = p_scale;
_game_time_scale = p_scale;
_update_time_scale();
}

double Engine::get_time_scale() const {
return freeze_time_scale ? 0 : _time_scale;
return freeze_time_scale ? 0.0 : _game_time_scale;
}

void Engine::set_user_time_scale(double p_scale) {
_user_time_scale = p_scale;
_update_time_scale();
}

double Engine::get_effective_time_scale() const {
return freeze_time_scale ? 0.0 : _time_scale;
}

double Engine::get_unfrozen_time_scale() const {
Expand Down
11 changes: 11 additions & 0 deletions core/config/engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,17 @@ class Engine {
double _process_step = 0;

int ips = 60;
int user_ips = 60;
double physics_jitter_fix = 0.5;
double _fps = 1;
int _max_fps = 0;
int _audio_output_latency = 0;
double _time_scale = 1.0;
double _game_time_scale = 1.0;
double _user_time_scale = 1.0;
uint64_t _physics_frames = 0;
int max_physics_steps_per_frame = 8;
int max_user_physics_steps_per_frame = 8;
double _physics_interpolation_fraction = 0.0f;
bool abort_on_gpu_errors = false;
bool use_validation_layers = false;
Expand Down Expand Up @@ -109,14 +113,19 @@ class Engine {

bool freeze_time_scale = false;

protected:
void _update_time_scale();

public:
static Engine *get_singleton();

virtual void set_physics_ticks_per_second(int p_ips);
virtual int get_physics_ticks_per_second() const;
virtual int get_user_physics_ticks_per_second() const;

virtual void set_max_physics_steps_per_frame(int p_max_physics_steps);
virtual int get_max_physics_steps_per_frame() const;
virtual int get_user_max_physics_steps_per_frame() const;

void set_physics_jitter_fix(double p_threshold);
double get_physics_jitter_fix() const;
Expand All @@ -140,6 +149,8 @@ class Engine {

void set_time_scale(double p_scale);
double get_time_scale() const;
void set_user_time_scale(double p_scale);
double get_effective_time_scale() const;
double get_unfrozen_time_scale() const;

void set_print_to_stdout(bool p_enabled);
Expand Down
101 changes: 100 additions & 1 deletion editor/run/game_view_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,28 @@ void GameViewDebugger::next_frame() {
}
}

void GameViewDebugger::set_time_scale(double p_scale) {
Array message;
message.append(p_scale);

for (Ref<EditorDebuggerSession> &I : sessions) {
if (I->is_active()) {
I->send_message("scene:speed_changed", message);
}
}
}

void GameViewDebugger::reset_time_scale() {
Array message;
message.append(1.0);

for (Ref<EditorDebuggerSession> &I : sessions) {
if (I->is_active()) {
I->send_message("scene:speed_changed", message);
}
}
}

void GameViewDebugger::set_node_type(int p_type) {
node_type = p_type;

Expand Down Expand Up @@ -505,6 +527,8 @@ void GameView::_update_debugger_buttons() {

suspend_button->set_disabled(empty);
camera_override_button->set_disabled(empty);
speed_state_button->set_disabled(empty);
reset_speed_button->set_disabled(empty);

PopupMenu *menu = camera_override_menu->get_popup();

Expand All @@ -517,6 +541,8 @@ void GameView::_update_debugger_buttons() {
camera_override_button->set_pressed(false);
}
next_frame_button->set_disabled(!suspend_button->is_pressed());

_reset_time_scales();
}

void GameView::_handle_shortcut_requested(int p_embed_action) {
Expand Down Expand Up @@ -597,6 +623,51 @@ void GameView::_size_mode_button_pressed(int size_mode) {
_update_embed_window_size();
}

void GameView::_reset_time_scales() {
if (!is_visible_in_tree()) {
return;
}
time_scale_index = DEFAULT_TIME_SCALE_INDEX;
debugger->reset_time_scale();
_update_speed_buttons();
Comment on lines +626 to +632

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset the UI state when GameView is hidden.

Line 627 returns before resetting time_scale_index or button state. If a game restarts while GameView is hidden, the new session uses 1.0× but the GameView can still show the previous speed.

Reset the local state and button state before the visibility guard. Keep the debugger message behind the guard if that behavior is intentional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@editor/run/game_view_plugin.cpp` around lines 626 - 632, Update
GameView::_reset_time_scales() to reset time_scale_index and refresh the speed
buttons before the is_visible_in_tree() early return. Keep
debugger->reset_time_scale() behind the visibility guard so the debugger message
behavior remains unchanged.

}

void GameView::_speed_state_menu_pressed(int p_id) {
time_scale_index = p_id;
debugger->set_time_scale(time_scale_range[time_scale_index]);
_update_speed_buttons();
}

void GameView::_update_speed_buttons() {
bool disabled = time_scale_index == DEFAULT_TIME_SCALE_INDEX;
reset_speed_button->set_disabled(disabled);
speed_state_button->set_text(vformat(U"%s×", time_scale_label[time_scale_index]));
_update_speed_state_color();
}

void GameView::_update_speed_state_color() {
Color text_color;
if (time_scale_index == DEFAULT_TIME_SCALE_INDEX) {
text_color = get_theme_color(SceneStringName(font_color), EditorStringName(Editor));
} else if (time_scale_index > DEFAULT_TIME_SCALE_INDEX) {
text_color = get_theme_color(SNAME("success_color"), EditorStringName(Editor));
} else if (time_scale_index < DEFAULT_TIME_SCALE_INDEX) {
text_color = get_theme_color(SNAME("warning_color"), EditorStringName(Editor));
}
speed_state_button->add_theme_color_override(SceneStringName(font_color), text_color);
}

void GameView::_update_speed_state_size() {
if (!speed_state_button) {
return;
}
float min_size = 0;
for (const String lbl : time_scale_label) {
min_size = MAX(speed_state_button->get_minimum_size_for_text_and_icon(vformat(U"%s×", lbl), Ref<Texture2D>()).x, min_size);
}
speed_state_button->set_custom_minimum_size(Vector2(min_size, 0));
}

GameView::EmbedAvailability GameView::_get_embed_available() {
if (!DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_WINDOW_EMBEDDING)) {
return EMBED_NOT_AVAILABLE_FEATURE_NOT_SUPPORTED;
Expand Down Expand Up @@ -783,9 +854,14 @@ void GameView::_notification(int p_what) {
_update_ui();
} break;

case NOTIFICATION_POST_ENTER_TREE: {
_update_speed_state_size();
} break;

case NOTIFICATION_THEME_CHANGED: {
suspend_button->set_button_icon(get_editor_theme_icon(SNAME("Pause")));
next_frame_button->set_button_icon(get_editor_theme_icon(SNAME("NextFrame")));
reset_speed_button->set_button_icon(get_editor_theme_icon(SNAME("Reload")));

node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE]->set_button_icon(get_editor_theme_icon(SNAME("InputEventJoypadMotion")));
node_type_button[RuntimeNodeSelect::NODE_TYPE_2D]->set_button_icon(get_editor_theme_icon(SNAME("2DNodes")));
Expand All @@ -804,6 +880,9 @@ void GameView::_notification(int p_what) {

camera_override_button->set_button_icon(get_editor_theme_icon(SNAME("Camera")));
camera_override_menu->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl")));

_update_speed_state_size();
_update_speed_state_color();
} break;

case NOTIFICATION_READY: {
Expand Down Expand Up @@ -1081,6 +1160,26 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, EmbeddedProcessBase *p_embe
next_frame_button->set_accessibility_name(TTRC("Next Frame"));
next_frame_button->set_shortcut(ED_GET_SHORTCUT("editor/next_frame_embedded_project"));

speed_state_button = memnew(MenuButton);
main_menu_hbox->add_child(speed_state_button);
speed_state_button->set_text(U"1.0×");
speed_state_button->set_theme_type_variation(SceneStringName(FlatButton));
speed_state_button->set_tooltip_text(TTRC("Change the game speed."));
speed_state_button->set_accessibility_name(TTRC("Speed State"));

PopupMenu *menu = speed_state_button->get_popup();
menu->connect(SceneStringName(id_pressed), callable_mp(this, &GameView::_speed_state_menu_pressed));
for (String lbl : time_scale_label) {
menu->add_item(vformat(U"%s×", lbl));
}

reset_speed_button = memnew(Button);
main_menu_hbox->add_child(reset_speed_button);
reset_speed_button->set_theme_type_variation(SceneStringName(FlatButton));
reset_speed_button->set_tooltip_text(TTRC("Reset the game speed."));
reset_speed_button->set_accessibility_name(TTRC("Reset Speed"));
reset_speed_button->connect(SceneStringName(pressed), callable_mp(this, &GameView::_reset_time_scales));

main_menu_hbox->add_child(memnew(VSeparator));

node_type_button[RuntimeNodeSelect::NODE_TYPE_NONE] = memnew(Button);
Expand Down Expand Up @@ -1162,7 +1261,7 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, EmbeddedProcessBase *p_embe
camera_override_menu->set_h_size_flags(SIZE_SHRINK_END);
camera_override_menu->set_tooltip_text(TTRC("Camera Override Options"));

PopupMenu *menu = camera_override_menu->get_popup();
menu = camera_override_menu->get_popup();
menu->connect(SceneStringName(id_pressed), callable_mp(this, &GameView::_camera_override_menu_id_pressed));
menu->add_item(TTRC("Reset 2D Camera"), CAMERA_RESET_2D);
menu->add_item(TTRC("Reset 3D Camera"), CAMERA_RESET_3D);
Expand Down
17 changes: 17 additions & 0 deletions editor/run/game_view_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ class GameViewDebugger : public EditorDebuggerPlugin {
void set_suspend(bool p_enabled);
void next_frame();

void set_time_scale(double p_scale);
void reset_time_scale();

void set_node_type(int p_type);
void set_select_mode(int p_mode);

Expand Down Expand Up @@ -180,6 +183,14 @@ class GameView : public VBoxContainer {
EmbeddedProcessBase *embedded_process = nullptr;
Label *state_label = nullptr;

int const DEFAULT_TIME_SCALE_INDEX = 5;
Array time_scale_range = { 0.0625f, 0.125f, 0.25f, 0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 4.0f, 8.0f, 16.0f };
Array time_scale_label = { "1/16", "1/8", "1/4", "1/2", "3/4", "1.0", "1.25", "1.5", "1.75", "2.0", "4.0", "8.0", "16.0" };
int time_scale_index = DEFAULT_TIME_SCALE_INDEX;

MenuButton *speed_state_button = nullptr;
Button *reset_speed_button = nullptr;

void _sessions_changed();

void _update_debugger_buttons();
Expand All @@ -193,6 +204,12 @@ class GameView : public VBoxContainer {
void _embed_options_menu_menu_id_pressed(int p_id);
void _size_mode_button_pressed(int size_mode);

void _reset_time_scales();
void _speed_state_menu_pressed(int p_id);
void _update_speed_buttons();
void _update_speed_state_color();
void _update_speed_state_size();

void _play_pressed();
static void _instance_starting_static(int p_idx, List<String> &r_arguments);
void _instance_starting(int p_idx, List<String> &r_arguments);
Expand Down
6 changes: 3 additions & 3 deletions main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4838,10 +4838,10 @@ bool Main::iteration() {

const uint64_t ticks_elapsed = ticks - last_ticks;

const int physics_ticks_per_second = Engine::get_singleton()->get_physics_ticks_per_second();
const int physics_ticks_per_second = Engine::get_singleton()->get_user_physics_ticks_per_second();
const double physics_step = 1.0 / physics_ticks_per_second;

const double time_scale = Engine::get_singleton()->get_time_scale();
const double time_scale = Engine::get_singleton()->get_effective_time_scale();

MainFrameTime advance = main_timer_sync.advance(physics_step, physics_ticks_per_second);
double process_step = advance.process_step;
Expand All @@ -4860,7 +4860,7 @@ bool Main::iteration() {

last_ticks = ticks;

const int max_physics_steps = Engine::get_singleton()->get_max_physics_steps_per_frame();
const int max_physics_steps = Engine::get_singleton()->get_user_max_physics_steps_per_frame();
if (fixed_fps == -1 && advance.physics_steps > max_physics_steps) {
process_step -= (advance.physics_steps - max_physics_steps) * physics_step;
advance.physics_steps = max_physics_steps;
Expand Down
4 changes: 2 additions & 2 deletions modules/jolt_physics/joints/jolt_hinge_joint_3d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ constexpr double HINGE_DEFAULT_RELAXATION = 1.0;
double estimate_physics_step() {
Engine *engine = Engine::get_singleton();

const double step = 1.0 / engine->get_physics_ticks_per_second();
const double step_scaled = step * engine->get_time_scale();
const double step = 1.0 / engine->get_user_physics_ticks_per_second();
const double step_scaled = step * engine->get_effective_time_scale();
Comment on lines +60 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Refresh hinge motor torque when the effective step changes.

Line 61 now changes during a game-view speed update. set_param() converts HINGE_JOINT_MOTOR_MAX_IMPULSE to motor_max_torque only once at lines 246-249. A later speed change leaves that stored torque stale. get_param() then reports a different impulse value at lines 202-204.

Store the requested impulse and recalculate the Jolt torque limit when the effective physics step changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/jolt_physics/joints/jolt_hinge_joint_3d.cpp` around lines 60 - 61,
Update the hinge joint motor state so the requested
HINGE_JOINT_MOTOR_MAX_IMPULSE is retained separately from the derived
motor_max_torque. In set_param(), store the requested impulse and derive the
torque using the current effective step; when step_scaled changes in the physics
update path, recalculate motor_max_torque from that stored impulse. Ensure
get_param() continues converting the refreshed torque back to the requested
impulse consistently.


return step_scaled;
}
Expand Down
4 changes: 2 additions & 2 deletions scene/3d/velocity_tracker_3d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Vector3 VelocityTracker3D::get_tracked_linear_velocity() const {
if (position_history_len) {
if (physics_step) {
uint64_t base = Engine::get_singleton()->get_physics_frames();
base_time = double(base - position_history[0].frame) / Engine::get_singleton()->get_physics_ticks_per_second();
base_time = double(base - position_history[0].frame) / Engine::get_singleton()->get_user_physics_ticks_per_second();
} else {
uint64_t base = Engine::get_singleton()->get_frame_ticks();
base_time = double(base - position_history[0].frame) / 1000000.0;
Expand All @@ -92,7 +92,7 @@ Vector3 VelocityTracker3D::get_tracked_linear_velocity() const {
Vector3 distance = position_history[i].position - position_history[i + 1].position;

if (physics_step) {
delta = double(diff) / Engine::get_singleton()->get_physics_ticks_per_second();
delta = double(diff) / Engine::get_singleton()->get_user_physics_ticks_per_second();
} else {
delta = double(diff) / 1000000.0;
}
Expand Down
8 changes: 8 additions & 0 deletions scene/debugger/scene_debugger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ Error SceneDebugger::_msg_next_frame(const Array &p_args) {
return OK;
}

Error SceneDebugger::_msg_speed_changed(const Array &p_args) {
ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
double time_scale_user = p_args[0];
Engine::get_singleton()->set_user_time_scale(time_scale_user);
return OK;
Comment on lines +216 to +220

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate the time-scale value before applying it.

Line 217 only rejects an empty array. A non-finite value can reach Engine::set_user_time_scale() and propagate through runtime timing calculations.

Require one numeric, finite argument before updating the engine.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scene/debugger/scene_debugger.cpp` around lines 216 - 220, Update
SceneDebugger::_msg_speed_changed to require exactly one numeric argument and
validate that the converted time-scale value is finite before calling
Engine::get_singleton()->set_user_time_scale. Return ERR_INVALID_DATA for empty,
extra, non-numeric, or non-finite input, while preserving the existing
successful update path for valid values.

}

Error SceneDebugger::_msg_debug_mute_audio(const Array &p_args) {
ERR_FAIL_COND_V(p_args.is_empty(), ERR_INVALID_DATA);
bool do_mute = p_args[0];
Expand Down Expand Up @@ -530,6 +537,7 @@ void SceneDebugger::_init_message_handlers() {
message_handlers["clear_selection"] = _msg_clear_selection;
message_handlers["suspend_changed"] = _msg_suspend_changed;
message_handlers["next_frame"] = _msg_next_frame;
message_handlers["speed_changed"] = _msg_speed_changed;
message_handlers["debug_mute_audio"] = _msg_debug_mute_audio;
message_handlers["override_cameras"] = _msg_override_cameras;
message_handlers["transform_camera_2d"] = _msg_transform_camera_2d;
Expand Down
1 change: 1 addition & 0 deletions scene/debugger/scene_debugger.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class SceneDebugger {
static Error _msg_clear_selection(const Array &p_args);
static Error _msg_suspend_changed(const Array &p_args);
static Error _msg_next_frame(const Array &p_args);
static Error _msg_speed_changed(const Array &p_args);
static Error _msg_debug_mute_audio(const Array &p_args);
static Error _msg_override_cameras(const Array &p_args);
static Error _msg_transform_camera_2d(const Array &p_args);
Expand Down
2 changes: 1 addition & 1 deletion scene/gui/video_stream_player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ void VideoStreamPlayer::_notification(int p_notification) {
double delta = first_frame ? 0 : get_process_delta_time();
first_frame = false;

resampler.set_playback_speed(Engine::get_singleton()->get_time_scale() * speed_scale);
resampler.set_playback_speed(Engine::get_singleton()->get_effective_time_scale() * speed_scale);

playback->update(delta * speed_scale); // playback->is_playing() returns false in the last video frame

Expand Down
Loading