Skip to content
Snippets Groups Projects
Commit c4a3cf9d authored by Annika Österdiekhoff's avatar Annika Österdiekhoff
Browse files

Merge branch '80-controller-support' into 'main'

Resolve "Controller Support"

Closes #80

See merge request scs/cocosy/overcooked-simulator!45
parents 41b0065a 5bce4290
No related branches found
No related tags found
1 merge request!45Resolve "Controller Support"
Pipeline #46833 passed
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()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment