From b187b965073ad9c25d904b27aec15f93a078955e Mon Sep 17 00:00:00 2001 From: Yan Wittmann Date: Mon, 13 Jan 2025 19:30:46 +0100 Subject: [PATCH] Added graphedit node highlighting and scrolling --- project/main-scenes/island.tscn | 5 +- project/scripts/global/GameManager.gd | 192 +++++++++--------- project/scripts/player/tree/Task.gd | 22 +- .../player/tree/impl/base/TaskSelector.gd | 4 +- .../player/tree/impl/base/TaskSequence.gd | 4 +- .../visualization/BehaviorTreeVisualizer.gd | 49 ++++- project/scripts/visualization/d_tree_node.gd | 25 ++- 7 files changed, 177 insertions(+), 124 deletions(-) diff --git a/project/main-scenes/island.tscn b/project/main-scenes/island.tscn index bae31a2..debaf69 100644 --- a/project/main-scenes/island.tscn +++ b/project/main-scenes/island.tscn @@ -216,8 +216,8 @@ size_flags_vertical = 8 [node name="TreeVisualizer" type="Window" parent="Camera2D/CanvasLayer"] unique_name_in_owner = true -position = Vector2i(0, 36) -size = Vector2i(1075, 225) +position = Vector2i(520, 41) +size = Vector2i(615, 595) script = ExtResource("5_ecfvx") [node name="GraphEdit" type="GraphEdit" parent="Camera2D/CanvasLayer/TreeVisualizer"] @@ -229,6 +229,7 @@ grow_horizontal = 2 grow_vertical = 2 size_flags_horizontal = 3 size_flags_vertical = 3 +scroll_offset = Vector2(-77, -25) show_menu = false show_zoom_buttons = false show_grid_buttons = false diff --git a/project/scripts/global/GameManager.gd b/project/scripts/global/GameManager.gd index 919bef7..d041942 100644 --- a/project/scripts/global/GameManager.gd +++ b/project/scripts/global/GameManager.gd @@ -24,157 +24,159 @@ var waiting_for_input: bool = true func _ready() -> void: - tilemap_navigation.world = world - tilemap_navigation.player = player - player.game_manager = self - world.camp_manager.game_manager = self - world.step_visualizer.game_manager = self - world.step_visualizer.world = world - update_bars() - call_deferred("defer_ready") + tilemap_navigation.world = world + tilemap_navigation.player = player + player.game_manager = self + world.camp_manager.game_manager = self + world.step_visualizer.game_manager = self + world.step_visualizer.world = world + update_bars() + call_deferred("defer_ready") func defer_ready() -> void: - tree_visualizer.behavior_tree = player.behavior_tree - tree_visualizer.build_tree() + tree_visualizer.behavior_tree = player.behavior_tree + tree_visualizer.build_tree() - intro_image.visible = true - await wait_for_key_press() - get_tree().create_tween().tween_method(set_intro_opacity, 1.0, 0.0, 1.0) + intro_image.visible = true + await wait_for_key_press() + get_tree().create_tween().tween_method(set_intro_opacity, 1.0, 0.0, 1.0) # game_ticker.start() func _process(delta: float) -> void: - if Input.is_action_just_pressed("force_game_tick"): - Task.print_behavior_tree_evaluation = true - _on_game_tick_timeout() - Task.print_behavior_tree_evaluation = false - if Input.is_action_pressed("force_game_tick_fast"): - _on_game_tick_timeout() - if Input.is_action_just_pressed("key_2"): - toggle_temperature_layer() - camera.print_config() - if Input.is_action_just_pressed("auto_tick"): - if game_ticker.is_stopped(): - game_ticker.start() - else: - game_ticker.stop() + if Input.is_action_just_pressed("force_game_tick"): + Task.print_behavior_tree_evaluation = true + _on_game_tick_timeout() + Task.print_behavior_tree_evaluation = false + if Input.is_action_pressed("force_game_tick_fast"): + _on_game_tick_timeout() + if Input.is_action_just_pressed("key_2"): + toggle_temperature_layer() + camera.print_config() + if Input.is_action_just_pressed("auto_tick"): + if game_ticker.is_stopped(): + game_ticker.start() + else: + game_ticker.stop() - if intro_image.is_visible(): - intro_image.set_scale(calculate_scale(intro_image.texture.get_size())) + if intro_image.is_visible(): + intro_image.set_scale(calculate_scale(intro_image.texture.get_size())) func calculate_scale(image_size: Vector2) -> Vector2: - var viewport_size: Vector2 = world.get_viewport_rect().size - var scale: float = viewport_size.x / image_size.x - return Vector2(scale, scale) + var viewport_size: Vector2 = world.get_viewport_rect().size + var scale: float = viewport_size.x / image_size.x + return Vector2(scale, scale) # SECTION: intro func set_intro_opacity(opacity: float) -> void: - intro_image.set_modulate(Color(1, 1, 1, opacity)) + intro_image.set_modulate(Color(1, 1, 1, opacity)) # SECTION: game tick func player_health_depleted(): - # TODO - pass + # TODO + pass func _on_game_tick_timeout() -> void: - var timer_on_game_tick_timeout: PerformanceTimer = PerformanceTimer.new() - timer_on_game_tick_timeout.display_name = "frame" + var timer_on_game_tick_timeout: PerformanceTimer = PerformanceTimer.new() + timer_on_game_tick_timeout.display_name = "frame" - tilemap_navigation.game_tick_start() - world.game_tick_start() + tilemap_navigation.game_tick_start() + world.game_tick_start() - player.game_tick() + player.game_tick() - tilemap_navigation.game_tick_end() - world.game_tick_end() - EventsTracker.populate_visual_log(%RecentEventsLog, self) + tree_visualizer.update_task_statuses(player.behavior_tree.blackboard) - update_bars() - handle_result_game_state(player.behavior_tree.blackboard) + tilemap_navigation.game_tick_end() + world.game_tick_end() + EventsTracker.populate_visual_log(%RecentEventsLog, self) - if not game_ticker.is_stopped(): - camera_follow_player() + update_bars() + handle_result_game_state(player.behavior_tree.blackboard) - timer_on_game_tick_timeout.stop() + if not game_ticker.is_stopped(): + camera_follow_player() + + timer_on_game_tick_timeout.stop() func camera_follow_player() -> void: - var player_position: Vector2 = world.tilemap_player.cell_to_local(player.board_position) - var targeted_position = null + var player_position: Vector2 = world.tilemap_player.cell_to_local(player.board_position) + var targeted_position = null - if player.behavior_tree.blackboard.has("path"): - var path: Array = player.behavior_tree.blackboard["path"] - if path.size() > 0: - targeted_position = world.tilemap_player.cell_to_local(path[path.size() - 1]) + if player.behavior_tree.blackboard.has("path"): + var path: Array = player.behavior_tree.blackboard["path"] + if path.size() > 0: + targeted_position = world.tilemap_player.cell_to_local(path[path.size() - 1]) - if not targeted_position: - camera.go_to(player_position) - return + if not targeted_position: + camera.go_to(player_position) + return - var avg_position = (player_position + targeted_position) / 2 - var distance: float = player_position.distance_to(targeted_position) - if distance < 200: - camera.go_to_zooming(avg_position, distance_to_zoom_level(200)) - else: - var zoom_level: float = distance_to_zoom_level(distance) - camera.go_to_zooming(avg_position, zoom_level) + var avg_position = (player_position + targeted_position) / 2 + var distance: float = player_position.distance_to(targeted_position) + if distance < 200: + camera.go_to_zooming(avg_position, distance_to_zoom_level(200)) + else: + var zoom_level: float = distance_to_zoom_level(distance) + camera.go_to_zooming(avg_position, zoom_level) func distance_to_zoom_level(distance: float) -> float: - var a: float = 862.08 - var b: float = 274.13 - return a / (distance + b) + var a: float = 862.08 + var b: float = 274.13 + return a / (distance + b) func handle_result_game_state(blackboard: Dictionary) -> void: - if blackboard.has("game_state_win"): - EventsTracker.track(EventsTracker.Event.GAME_STATE_WIN) - game_ticker.stop() + if blackboard.has("game_state_win"): + EventsTracker.track(EventsTracker.Event.GAME_STATE_WIN) + game_ticker.stop() func update_bars() -> void: - if health_bar != null: - health_bar.max_value = player.max_health - health_bar.value = clamp(player.health, 0, player.max_health) - %HealthLabel.text = str(health_bar.value) + "/" + str(player.max_health) - %HealthLabel.add_theme_color_override("font_color", Color(1, 1, 1)) + if health_bar != null: + health_bar.max_value = player.max_health + health_bar.value = clamp(player.health, 0, player.max_health) + %HealthLabel.text = str(health_bar.value) + "/" + str(player.max_health) + %HealthLabel.add_theme_color_override("font_color", Color(1, 1, 1)) - if food_bar != null: - food_bar.max_value = player.max_food - food_bar.value = clamp(player.food, 0, player.max_food) - %FoodLabel.text = str(food_bar.value) + "/" + str(player.max_food) + if food_bar != null: + food_bar.max_value = player.max_food + food_bar.value = clamp(player.food, 0, player.max_food) + %FoodLabel.text = str(food_bar.value) + "/" + str(player.max_food) - if temperature_resistance_bar != null: - temperature_resistance_bar.max_value = player.temperature_set_buff_value - temperature_resistance_bar.value = clamp(player.temperature_buff_timer, 0, player.temperature_set_buff_value) - %TemperatureResistanceLabel.text = str(temperature_resistance_bar.value) + "/" + str(player.temperature_set_buff_value) + if temperature_resistance_bar != null: + temperature_resistance_bar.max_value = player.temperature_set_buff_value + temperature_resistance_bar.value = clamp(player.temperature_buff_timer, 0, player.temperature_set_buff_value) + %TemperatureResistanceLabel.text = str(temperature_resistance_bar.value) + "/" + str(player.temperature_set_buff_value) - if temperature_bar != null: - temperature_bar.max_value = player.temperature_endure - # invert the value to show the time left - var countdown: int = player.temperature_endure - player.temperature_timer - temperature_bar.value = clamp(countdown, 0, player.temperature_endure) - %TemperatureLabel.text = str(temperature_bar.value) + "/" + str(player.temperature_endure) + if temperature_bar != null: + temperature_bar.max_value = player.temperature_endure + # invert the value to show the time left + var countdown: int = player.temperature_endure - player.temperature_timer + temperature_bar.value = clamp(countdown, 0, player.temperature_endure) + %TemperatureLabel.text = str(temperature_bar.value) + "/" + str(player.temperature_endure) func toggle_temperature_layer() -> void: - world.tilemap_temperature.tilemap.visible = not world.tilemap_temperature.tilemap.visible + world.tilemap_temperature.tilemap.visible = not world.tilemap_temperature.tilemap.visible func wait_for_key_press(): - waiting_for_input = true - while waiting_for_input: - await get_tree().process_frame + waiting_for_input = true + while waiting_for_input: + await get_tree().process_frame func _input(event): - if event is InputEventKey and event.pressed: - waiting_for_input = false + if event is InputEventKey and event.pressed: + waiting_for_input = false diff --git a/project/scripts/player/tree/Task.gd b/project/scripts/player/tree/Task.gd index d0c09d0..ae1a354 100644 --- a/project/scripts/player/tree/Task.gd +++ b/project/scripts/player/tree/Task.gd @@ -70,15 +70,7 @@ func get_first_child() -> Task: func human_readable(addon: String = "") -> String: - var clear_status: String = "UNKNOWN" - if status == FAILURE: - clear_status = "FAILURE" - elif status == SUCCESS: - clear_status = "SUCCESS" - elif status == RUNNING: - clear_status = "RUNNING" - elif status == SUCCESS_STOP: - clear_status = "SUCCESS_STOP" + var clear_status: String = clear_status() var ret: String = name; if addon != "": @@ -91,6 +83,18 @@ func human_readable(addon: String = "") -> String: return ret +func clear_status() -> String: + if status == FAILURE: + return "FAILURE" + elif status == SUCCESS: + return "SUCCESS" + elif status == RUNNING: + return "RUNNING" + elif status == SUCCESS_STOP: + return "SUCCESS_STOP" + return "UNKNOWN" + + # SECTION: utility func find_closest_item(blackboard: Dictionary, item_types: Array[Vector2i], memory_key: String, max_distance: int = -1) -> Dictionary: diff --git a/project/scripts/player/tree/impl/base/TaskSelector.gd b/project/scripts/player/tree/impl/base/TaskSelector.gd index e6ae376..9c13254 100644 --- a/project/scripts/player/tree/impl/base/TaskSelector.gd +++ b/project/scripts/player/tree/impl/base/TaskSelector.gd @@ -13,11 +13,11 @@ func run(blackboard: Dictionary) -> void: if c.status == SUCCESS_STOP: c.status = SUCCESS status = SUCCESS - status_reason = "stopping at child " + c.name + ", as it returned SUCCESS_STOP" + status_reason = "stopping at " + c.name + " (STOP)" return if c.status != FAILURE: status = c.status - status_reason = "stopped at child " + c.name + status_reason = "stopped at " + c.name return status = FAILURE status_reason = "all children failed" diff --git a/project/scripts/player/tree/impl/base/TaskSequence.gd b/project/scripts/player/tree/impl/base/TaskSequence.gd index f3db4bc..796b83b 100644 --- a/project/scripts/player/tree/impl/base/TaskSequence.gd +++ b/project/scripts/player/tree/impl/base/TaskSequence.gd @@ -13,11 +13,11 @@ func run(blackboard: Dictionary) -> void: if c.status == SUCCESS_STOP: c.status = SUCCESS status = SUCCESS - status_reason = "stopping at child " + c.name + ", as it returned SUCCESS_STOP" + status_reason = "stopping at " + c.name + " (STOP)" return if c.status != SUCCESS: status = c.status - status_reason = "stopped at child " + c.name + status_reason = "stopped at " + c.name return status = SUCCESS status_reason = "all children succeeded" diff --git a/project/scripts/visualization/BehaviorTreeVisualizer.gd b/project/scripts/visualization/BehaviorTreeVisualizer.gd index 2e53466..250c41d 100644 --- a/project/scripts/visualization/BehaviorTreeVisualizer.gd +++ b/project/scripts/visualization/BehaviorTreeVisualizer.gd @@ -7,10 +7,13 @@ const D_TREE_NODE: PackedScene = preload("res://scripts/visualization/d_tree_nod # var behavior_tree: BehaviorTree # -var x_spacing: int = 400 +var x_spacing: int = 430 var y_spacing: int = 100 # var all_nodes: Array[DTreeNode] = [] +# Dictionary[Task, DTreeNode] +var task_to_node: Dictionary = {} +var parent_nodes: Dictionary = {} # var current_lowest_node_pos: Vector2 = Vector2(0, 0) @@ -46,13 +49,15 @@ func build_tree_from_task(task: Task, depth: int) -> DTreeNode: var current_node: DTreeNode = D_TREE_NODE.instantiate() graph_edit.add_child(current_node) + task_to_node[task] = current_node current_node.name = task.get_name() + str(randf()) - current_node.title = transform_string(task.get_name()) + current_node.title = human_readable_task_name(task.get_name()) current_node.add_label("status", true, true) all_nodes.append(current_node) for child in child_nodes: graph_edit.connect_node(current_node.name, 0, child.name, 0) + parent_nodes[child] = current_node if child_node_positions.size() > 0: var average_position: Vector2 = Vector2(0, 0) @@ -72,7 +77,43 @@ func build_tree_from_task(task: Task, depth: int) -> DTreeNode: return current_node -func transform_string(input: String) -> String: + +func update_task_statuses(blackboard: Dictionary) -> void: + for t in task_to_node.keys(): + var task: Task = t as Task + var node: DTreeNode = task_to_node[task] + var status: int = task.status + var clear_status: String = task.clear_status() + var status_reason: String = task.status_reason + + if status_reason != "": + node.set_label_text(0, status_reason) + else: + node.set_label_text(0, clear_status) + + node.set_body_color(node.color_normal) + if status == Task.RUNNING or status == Task.SUCCESS or status == Task.SUCCESS_STOP: + node.set_body_color(node.color_success) + + if blackboard.has("current_task"): + var selected_node = task_to_node[blackboard["current_task"]] + if selected_node: + center_view_on_position(selected_node.position_offset) + selected_node.set_body_color(selected_node.color_executed) + while parent_nodes.has(selected_node): + selected_node = parent_nodes[selected_node] + selected_node.set_body_color(selected_node.color_checked) + + +func center_view_on_position(target_position: Vector2) -> void: + var graph_edit_size: Vector2 = graph_edit.size + var zoom: float = graph_edit.zoom + var offset_x: float = target_position.x * zoom - graph_edit_size.x / 2 + var offset_y: float = target_position.y * zoom - graph_edit_size.y / 2 + graph_edit.scroll_offset = Vector2(offset_x, offset_y) + + +func human_readable_task_name(input: String) -> String: var prefixes: Dictionary = {"sl_": "Selector: ", "sq_": "Sequence: ", "Task": ""} var selected_prefix: String = "" @@ -86,7 +127,7 @@ func transform_string(input: String) -> String: var current_word: String = "" for i in range(input.length()): - var character = input[i] + var character: String = input[i] if character.to_upper() == character and current_word.length() > 0: words.append(current_word) current_word = "" + character.to_lower() diff --git a/project/scripts/visualization/d_tree_node.gd b/project/scripts/visualization/d_tree_node.gd index c5172ff..c3e0798 100644 --- a/project/scripts/visualization/d_tree_node.gd +++ b/project/scripts/visualization/d_tree_node.gd @@ -1,15 +1,19 @@ class_name DTreeNode extends GraphNode -var left_slots: Array[String] = [] -var right_slots: Array[String] = [] -var color_highlighted: StyleBoxFlat = StyleBoxFlat.new() -var color_normal: StyleBoxFlat = StyleBoxFlat.new() +var left_slots: Array[String] = [] +var right_slots: Array[String] = [] +var color_normal: StyleBoxFlat = StyleBoxFlat.new() +var color_success: StyleBoxFlat = StyleBoxFlat.new() +var color_executed: StyleBoxFlat = StyleBoxFlat.new() +var color_checked: StyleBoxFlat = StyleBoxFlat.new() func _ready() -> void: - color_highlighted.bg_color = Color(0.67058825, 1.0, 0.5411765) color_normal.bg_color = Color(1.0, 1.0, 1.0) + color_success.bg_color = Color(0.25490198, 0.78431374, 0.9529412) + color_executed.bg_color = Color(0.5058824, 0.9529412, 0.30588236) + color_checked.bg_color = Color(0.6509804, 0.9254902, 0.5372549) func add_label(label_text: String, left: bool, right: bool) -> Vector3i: @@ -33,8 +37,9 @@ func add_label(label_text: String, left: bool, right: bool) -> Vector3i: return Vector3i(child_index, left_slots.size() - 1, right_slots.size() - 1) -func set_highlighted(highlight: bool) -> void: - if highlight: - self.add_theme_stylebox_override("panel", color_highlighted) - else: - self.add_theme_stylebox_override("panel", color_normal) +func set_body_color(color: StyleBoxFlat) -> void: + self.add_theme_stylebox_override("panel", color) + +func set_label_text(label_index: int, text: String) -> void: + var label: Label = self.get_child(label_index) as Label + label.text = text