diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py index a537861748079b3b6aaefc828b4b7942d41483ba..ad2cf169e30f1b48179a2302ca68b99d20765189 100644 --- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py +++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py @@ -1,6 +1,5 @@ import argparse import dataclasses -import glob import json import logging import random @@ -52,12 +51,13 @@ class PlayerKeySet: """ def __init__( - self, - move_keys: list[pygame.key], - interact_key: pygame.key, - pickup_key: pygame.key, - switch_key: pygame.key, - players: list[int], + self, + move_keys: list[pygame.key], + interact_key: pygame.key, + pickup_key: pygame.key, + switch_key: pygame.key, + players: list[int], + joystick: int ): """Creates a player key set which contains information about which keyboard keys control the player. @@ -69,6 +69,7 @@ class PlayerKeySet: pickup_key: The key to pick items up or put them down. switch_key: The key for switching through controllable players. players: The player indices which this keyset can control. + joystick: number of joystick (later check if available) """ self.move_vectors: list[list[int]] = [[-1, 0], [1, 0], [0, -1], [0, 1]] self.key_to_movement: dict[pygame.key, list[int]] = { @@ -82,6 +83,7 @@ class PlayerKeySet: self.current_player: int = players[0] if players else 0 self.current_idx = 0 self.other_keyset: list[PlayerKeySet] = [] + self.joystick = joystick def set_controlled_players(self, controlled_players: list[int]) -> None: self.controlled_players = controlled_players @@ -102,10 +104,10 @@ class PyGameGUI: """Visualisation of the overcooked environment and reading keyboard inputs using pygame.""" def __init__( - self, - url: str, - port: int, - manager_ids: list[str], + self, + url: str, + port: int, + manager_ids: list[str], ): pygame.init() pygame.display.set_icon( @@ -160,10 +162,10 @@ class PyGameGUI: self.kitchen_height = state["kitchen"]["height"] self.kitchen_aspect_ratio = self.kitchen_height / self.kitchen_width game_width = self.visualization_config["GameWindow"]["min_width"] - ( - 2 * self.screen_margin + 2 * self.screen_margin ) game_height = self.visualization_config["GameWindow"]["min_height"] - ( - 2 * self.screen_margin + 2 * self.screen_margin ) if self.kitchen_width > game_width: @@ -200,6 +202,9 @@ class PyGameGUI: self.game_height -= residual_y def setup_player_keys(self, n=1, disjunct=False): + # First four keys are for movement. Order: Down, Up, Left, Right. + # 5th key is for interacting with counters. + # 6th key ist for picking up things or dropping them. if n: players = list(range(self.number_humans_to_be_added)) key_set1 = PlayerKeySet( @@ -208,6 +213,7 @@ class PyGameGUI: pickup_key=pygame.K_e, switch_key=pygame.K_SPACE, players=players, + joystick=0 ) key_set2 = PlayerKeySet( move_keys=[pygame.K_LEFT, pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN], @@ -215,6 +221,7 @@ class PyGameGUI: pickup_key=pygame.K_o, switch_key=pygame.K_p, players=players, + joystick=1 ) key_sets = [key_set1, key_set2] @@ -256,6 +263,41 @@ class PyGameGUI: ) self.send_action(action) + def handle_joy_stick_input(self, joysticks): + """Handles joystick inputs for movement every frame + Args: + joysticks: list of joysticks + """ + # Axis 0: joy stick left: -1 = left, ~0 = center, 1 = right + # Axis 1: joy stick left: -1 = up, ~0 = center, 1 = down + # see control stuff here (at the end of the page): https://www.pygame.org/docs/ref/joystick.html + for key_set in self.key_sets: + current_player_name = str(key_set.current_player) + # if a joystick is connected for current player + if key_set.joystick in joysticks: + # Usually axis run in pairs, up/down for one, and left/right for the other. Triggers count as axes. + # You may want to take into account some tolerance to handle jitter, and + # joystick drift may keep the joystick from centering at 0 or using the full range of position values. + tolerance_threshold = 0.2 + # axis 0 = joy stick left --> left & right + axis_left_right = joysticks[key_set.joystick].get_axis(0) + axis_up_down = joysticks[key_set.joystick].get_axis(1) + if abs(axis_left_right) > tolerance_threshold or abs(axis_up_down) > tolerance_threshold: + move_vec = np.zeros(2) + if abs(axis_left_right) > tolerance_threshold: + move_vec[0] += axis_left_right + # axis 1 = joy stick right --> up & down + if abs(axis_up_down) > tolerance_threshold: + move_vec[1] += axis_up_down + + if np.linalg.norm(move_vec) != 0: + move_vec = move_vec / np.linalg.norm(move_vec) + + action = Action( + current_player_name, ActionType.MOVEMENT, move_vec, duration=self.time_delta + ) + self.send_action(action) + def handle_key_event(self, event): """Handles key events for the pickup and interaction keys. Pickup is a single action, for interaction keydown and keyup is necessary, because the player has to be able to hold @@ -286,6 +328,42 @@ class PyGameGUI: if event.type == pygame.KEYDOWN: key_set.next_player() + def handle_joy_stick_event(self, event, joysticks): + """Handles joy stick events for the pickup and interaction keys. Pickup is a single action, + for interaction buttondown and buttonup is necessary, because the player has to be able to hold + the button down. + + Args: + event: Pygame event for extracting the button action. + joysticks: list of joysticks + """ + + for key_set in self.key_sets: + current_player_name = str(key_set.current_player) + # if a joystick is connected for current player + if key_set.joystick in joysticks: + # pickup = Button A <-> 0 + if joysticks[key_set.joystick].get_button(0) and event.type == pygame.JOYBUTTONDOWN: + action = Action(current_player_name, ActionType.PUT, "pickup") + self.send_action(action) + + # interact = Button X <-> 2 + if joysticks[key_set.joystick].get_button(2) and event.type == pygame.JOYBUTTONDOWN: + action = Action( + current_player_name, ActionType.INTERACT, InterActionData.START + ) + self.send_action(action) + # stop interaction if last pressed button was X <-> 2 + if event.button == 2 and event.type == pygame.JOYBUTTONUP: + action = Action( + current_player_name, ActionType.INTERACT, InterActionData.STOP + ) + self.send_action(action) + # switch button Y <-> 3 + if joysticks[key_set.joystick].get_button(3) and not CONNECT_WITH_STUDY_SERVER: + if event.type == pygame.JOYBUTTONDOWN: + key_set.next_player() + def init_ui_elements(self): self.manager = pygame_gui.UIManager((self.window_width, self.window_height)) self.manager.get_theme().load_theme(ROOT_DIR / "gui_2d_vis" / "gui_theme.json") @@ -745,7 +823,7 @@ class PyGameGUI: self.player_info = {self.player_info["player_id"]: self.player_info} else: environment_config_path = ( - ROOT_DIR / "game_content" / "environment_config.yaml" + ROOT_DIR / "game_content" / "environment_config.yaml" ) layout_path = self.layout_file_paths[self.layout_selection.selected_option] item_info_path = ROOT_DIR / "game_content" / "item_info.yaml" @@ -789,7 +867,7 @@ class PyGameGUI: ) ) assert ( - json.loads(websocket.recv())["status"] == 200 + json.loads(websocket.recv())["status"] == 200 ), "not accepted player" self.websockets[player_id] = websocket else: @@ -865,13 +943,13 @@ class PyGameGUI: self.menu_state = MenuStates.Game self.number_players = ( - self.number_humans_to_be_added + self.number_bots_to_be_added + self.number_humans_to_be_added + self.number_bots_to_be_added ) self.vis.create_player_colors(self.number_players) if self.split_players: assert ( - self.number_humans_to_be_added > 1 + self.number_humans_to_be_added > 1 ), "Not enough players for key configuration." num_key_set = 2 if self.multiple_keysets else 1 self.key_sets = self.setup_player_keys( @@ -1035,6 +1113,11 @@ class PyGameGUI: # Game loop self.running = True + # This dict can be left as-is, since pygame will generate a + # pygame.JOYDEVICEADDED event for every joystick connected + # at the start of the program. + joysticks = {} + while self.running: try: self.time_delta = clock.tick(self.FPS) / 1000 @@ -1044,6 +1127,20 @@ class PyGameGUI: if event.type == pygame.QUIT: self.running = False + # connect joystick + if pygame.joystick.get_count() > 0 and event.type == pygame.JOYDEVICEADDED: + # This event will be generated when the program starts for every + # joystick, filling up the list without needing to create them manually. + joy = pygame.joystick.Joystick(event.device_index) + joysticks[joy.get_instance_id()] = joy + print(f"Joystick {joy.get_instance_id()} connected") + + # disconnect joystick + if event.type == pygame.JOYDEVICEREMOVED: + del joysticks[event.instance_id] + print(f"Joystick {event.instance_id} disconnected") + print("Number of joysticks:", pygame.joystick.get_count()) + # elif event.type == pygame.VIDEORESIZE: # # scrsize = event.size # self.window_width_windowed = event.w @@ -1057,8 +1154,8 @@ class PyGameGUI: match event.ui_element: case self.start_button: if not ( - self.number_humans_to_be_added - + self.number_bots_to_be_added + self.number_humans_to_be_added + + self.number_bots_to_be_added ): continue self.start_button_press() @@ -1122,12 +1219,14 @@ class PyGameGUI: self.manage_button_visibility() if ( - event.type in [pygame.KEYDOWN, pygame.KEYUP] - and self.menu_state == MenuStates.Game + event.type in [pygame.KEYDOWN, pygame.KEYUP] + and self.menu_state == MenuStates.Game ): - pass self.handle_key_event(event) + if event.type in [pygame.JOYBUTTONDOWN, pygame.JOYBUTTONUP] and self.menu_state == MenuStates.Game: + self.handle_joy_stick_event(event, joysticks=joysticks) + self.manager.process_events(event) # drawing: @@ -1144,6 +1243,7 @@ class PyGameGUI: state = self.request_state() self.handle_keys() + self.handle_joy_stick_input(joysticks=joysticks) if state["ended"]: self.finished_button_press()