From eababca6d1f18fa572853834b30ba76f0f78be68 Mon Sep 17 00:00:00 2001
From: fheinrich <fheinrich@techfak.uni-bielefeld.de>
Date: Fri, 19 Jan 2024 16:48:35 +0100
Subject: [PATCH] Movement based on timedelta, independent of gui fps

---
 overcooked_simulator/fastapi_game_server.py   | 14 ++++++---
 .../game_content/environment_config.yaml      |  2 +-
 .../gui_2d_vis/overcooked_gui.py              | 23 +++++++-------
 .../overcooked_environment.py                 | 30 ++++++++++++-------
 overcooked_simulator/player.py                | 10 ++++++-
 tests/test_start.py                           |  2 +-
 6 files changed, 53 insertions(+), 28 deletions(-)

diff --git a/overcooked_simulator/fastapi_game_server.py b/overcooked_simulator/fastapi_game_server.py
index 2a9fa234..a725684f 100644
--- a/overcooked_simulator/fastapi_game_server.py
+++ b/overcooked_simulator/fastapi_game_server.py
@@ -102,13 +102,17 @@ def parse_action(message: str) -> Action:
             value = np.array([x, y], dtype=float)
         else:
             value = None
-        action = Action(message_dict["player_name"], message_dict["act_type"], value)
+        action = Action(
+            message_dict["player_name"],
+            message_dict["act_type"],
+            value,
+            duration=message_dict["duration"],
+        )
         return action
 
 
 def manage_message(message: str):
     answer = None
-    print("MESSAGE:", message)
 
     if "get_state" in message:
         return oc_api.get_state()
@@ -119,7 +123,8 @@ def manage_message(message: str):
 
     action = parse_action(message)
     oc_api.simulator.enter_action(action)
-    return oc_api.get_state()
+    answer = oc_api.get_state()
+    return answer
 
 
 @app.get("/")
@@ -134,8 +139,9 @@ async def websocket_endpoint(websocket: WebSocket, client_id: int):
     try:
         while True:
             message = await websocket.receive_text()
+            # print("MESSAGE:", message)
             answer = manage_message(message)
-            print("ANSWER:", answer)
+            # print("ANSWER:", answer)
             await manager.send_personal_message(answer, websocket)
 
     except WebSocketDisconnect:
diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml
index a88b5d10..f23b3902 100644
--- a/overcooked_simulator/game_content/environment_config.yaml
+++ b/overcooked_simulator/game_content/environment_config.yaml
@@ -59,5 +59,5 @@ orders:
 
 player_config:
   radius: 0.4
-  move_dist: 0.15
+  player_speed_units_per_seconds: 5
   interaction_range: 1.6
\ No newline at end of file
diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
index b6e6cfdf..0500a3a6 100644
--- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py
+++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
@@ -201,7 +201,7 @@ class PyGameGUI:
 
         return player_colors
 
-    def handle_keys(self, websocket):
+    def handle_keys(self):
         """Handles keyboard inputs. Sends action for the respective players. When a key is held down, every frame
         an action is sent in this function.
         """
@@ -216,10 +216,12 @@ class PyGameGUI:
                 if np.linalg.norm(move_vec) != 0:
                     move_vec = move_vec / np.linalg.norm(move_vec)
 
-                action = Action(key_set.name, "movement", move_vec)
-                self.send_action(action, websocket)
+                action = Action(
+                    key_set.name, "movement", move_vec, duration=2 / (self.FPS)
+                )
+                self.send_action(action)
 
-    def handle_key_event(self, event, websocket):
+    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
         the key down.
@@ -230,15 +232,15 @@ class PyGameGUI:
         for key_set in self.player_key_sets:
             if event.key == key_set.pickup_key and event.type == pygame.KEYDOWN:
                 action = Action(key_set.name, "pickup", "pickup")
-                self.send_action(action, websocket)
+                self.send_action(action)
 
             if event.key == key_set.interact_key:
                 if event.type == pygame.KEYDOWN:
                     action = Action(key_set.name, "interact", "keydown")
-                    self.send_action(action, websocket)
+                    self.send_action(action)
                 elif event.type == pygame.KEYUP:
                     action = Action(key_set.name, "interact", "keyup")
-                    self.send_action(action, websocket)
+                    self.send_action(action)
 
     def draw_background(self):
         """Visualizes a game background."""
@@ -858,7 +860,7 @@ class PyGameGUI:
         self.reset_window_size()
         log.debug("Pressed finished button")
 
-    def send_action(self, action: Action, websocket):
+    def send_action(self, action: Action):
         """Sends an action to the game environment.
 
         Args:
@@ -872,6 +874,7 @@ class PyGameGUI:
             "player_name": action.player,
             "act_type": action.act_type,
             "value": value,
+            "duration": action.duration,
         }
         _ = self.websocket_communicate(message_dict)
 
@@ -938,7 +941,7 @@ class PyGameGUI:
                             and self.menu_state == MenuStates.Game
                         ):
                             pass
-                            self.handle_key_event(event, websocket)
+                            self.handle_key_event(event)
 
                         self.manager.process_events(event)
 
@@ -962,7 +965,7 @@ class PyGameGUI:
 
                             self.draw_background()
 
-                            self.handle_keys(websocket)
+                            self.handle_keys()
 
                             # state = self.simulator.get_state()
                             self.draw(state)
diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py
index 28e35742..2a63df64 100644
--- a/overcooked_simulator/overcooked_environment.py
+++ b/overcooked_simulator/overcooked_environment.py
@@ -40,7 +40,7 @@ log = logging.getLogger(__name__)
 class Action:
     """Action class, specifies player, action type and action itself."""
 
-    def __init__(self, player, act_type, action):
+    def __init__(self, player, act_type, action, duration=0):
         self.player = player
         self.act_type = act_type
         assert self.act_type in [
@@ -49,6 +49,7 @@ class Action:
             "interact",
         ], "Unknown action type"
         self.action = action
+        self.duration = duration
 
     def __repr__(self):
         return f"Action({self.player},{self.act_type},{self.action})"
@@ -335,8 +336,10 @@ class Environment:
         player = self.players[action.player]
 
         if action.act_type == "movement":
-            with self.lock:
-                self.perform_movement(player, action.action)
+            player.set_movement(
+                action.action,
+                self.env_time + datetime.timedelta(seconds=action.duration),
+            )
 
         else:
             counter = self.get_facing_counter(player)
@@ -351,10 +354,7 @@ class Environment:
                         player.last_interacted_counter = counter
             if action.action == "keyup":
                 if player.last_interacted_counter:
-                    with self.lock:
-                        player.perform_interact_hold_stop(
-                            player.last_interacted_counter
-                        )
+                    player.perform_interact_hold_stop(player.last_interacted_counter)
 
     def get_closest_counter(self, point: np.ndarray):
         """Determines the closest counter for a given 2d-coordinate point in the env.
@@ -385,7 +385,7 @@ class Environment:
         facing_counter = self.get_closest_counter(player.facing_point)
         return facing_counter
 
-    def perform_movement(self, player: Player, move_vector: np.array):
+    def perform_movement(self, player: Player, timedelta):
         """Moves a player in the direction specified in the action.action. If the player collides with a
         counter or other player through this movement, then they are not moved.
         (The extended code with the two ifs is for sliding movement at the counters, which feels a bit smoother.
@@ -402,7 +402,11 @@ class Environment:
         """
         old_pos = player.pos.copy()
 
-        step = move_vector * player.move_dist
+        move_vector = player.current_movement
+
+        d_time = timedelta.total_seconds()
+        step = move_vector * (player.move_speed * d_time)
+
         player.move(step)
         if self.detect_collision(player):
             collided_players = self.get_collided_players(player)
@@ -412,7 +416,8 @@ class Environment:
                     pushing_vector = pushing_vector / np.linalg.norm(pushing_vector)
 
                 old_pos_other = collided_player.pos.copy()
-                self.perform_movement(collided_player, pushing_vector)
+                collided_player.current_movement = pushing_vector
+                self.perform_movement(collided_player, timedelta)
                 if self.detect_collision_counters(
                     collided_player
                 ) or self.detect_collision_world_bounds(collided_player):
@@ -572,6 +577,10 @@ class Environment:
 
         with self.lock:
             if not self.game_ended:
+                for player in self.players.values():
+                    if self.env_time < player.movement_until:
+                        self.perform_movement(player, passed_time)
+
                 for counter in self.counters:
                     if isinstance(counter, (CuttingBoard, Stove, Sink, PlateDispenser)):
                         counter.progress(passed_time=passed_time, now=self.env_time)
@@ -663,7 +672,6 @@ class Environment:
                         closest_addon.pos - pos
                     ), f"No SinkAddon connected to Sink at pos {pos}"
                     counter.set_addon(closest_addon)
-
         pass
 
     @staticmethod
diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py
index 63f803c8..409ef065 100644
--- a/overcooked_simulator/player.py
+++ b/overcooked_simulator/player.py
@@ -1,3 +1,4 @@
+import datetime
 import logging
 from collections import deque
 from typing import Optional, Any
@@ -34,7 +35,7 @@ class Player:
         self.holding: Optional[Item] = None
 
         self.radius: float = self.player_config["radius"]
-        self.move_dist: int = self.player_config["move_dist"]
+        self.move_speed: int = self.player_config["player_speed_units_per_seconds"]
         self.interaction_range: int = self.player_config["interaction_range"]
         self.facing_direction: npt.NDArray[float] = np.array([0, 1])
         self.last_interacted_counter: Optional[
@@ -44,6 +45,13 @@ class Player:
         self.current_nearest_counter: Optional[Counter] = None
         self.facing_point: npt.NDArray[float] = np.zeros(2, float)
 
+        self.current_movement: npt.NDArray[2] = np.zeros(2, float)
+        self.movement_until: datetime.datetime = datetime.datetime.now()
+
+    def set_movement(self, move_vector, move_until):
+        self.current_movement = move_vector
+        self.movement_until = move_until
+
     def move(self, movement: npt.NDArray[float]):
         """Moves the player position by the given movement vector.
         A unit direction vector multiplied by move_dist is added to the player position.
diff --git a/tests/test_start.py b/tests/test_start.py
index d6010a9e..0f71d0e7 100644
--- a/tests/test_start.py
+++ b/tests/test_start.py
@@ -95,7 +95,7 @@ def test_movement():
         sim.enter_action(move_action)
 
     expected = start_pos + do_moves_number * (
-        move_direction * sim.env.players[player_name].move_dist
+        move_direction * sim.env.players[player_name].move_speed
     )
 
     assert np.isclose(
-- 
GitLab