diff --git a/arcade/__init__.py b/arcade/__init__.py index 838e87e49..fa24105ac 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -245,6 +245,8 @@ from .version import VERSION +from .paths import has_line_of_sight + __all__ = ['AnimatedTimeBasedSprite', 'AnimatedTimeSprite', @@ -385,6 +387,7 @@ 'get_tilemap_layer', 'get_viewport', 'get_window', + 'has_line_of_sight', 'is_point_in_polygon', 'isometric_grid_to_screen', 'lerp', diff --git a/arcade/examples/line_of_sight.py b/arcade/examples/line_of_sight.py new file mode 100644 index 000000000..398b09df4 --- /dev/null +++ b/arcade/examples/line_of_sight.py @@ -0,0 +1,236 @@ +""" +Line of Sight + +Artwork from http://kenney.nl + +If Python and Arcade are installed, this example can be run from the command line with: +python -m arcade.examples.line_of_sight +""" + +import arcade +import os +import random + +SPRITE_SCALING = 0.5 + +SCREEN_WIDTH = 800 +SCREEN_HEIGHT = 600 +SCREEN_TITLE = "Line of Sight" + +MOVEMENT_SPEED = 5 + +VIEWPORT_MARGIN = 300 + +class Player(arcade.Sprite): + """ Player sprite""" + def update(self): + """ Move player """ + self.center_x += self.change_x + self.center_y += self.change_y + + +class MyGame(arcade.Window): + """ + Main application class. + """ + + def __init__(self, width, height, title): + """ + Initializer + """ + + # Call the parent class initializer + super().__init__(width, height, title) + + # Set the working directory (where we expect to find files) to the same + # directory this .py file is in. You can leave this out of your own + # code, but it is needed to easily run the examples using "python -m" + # as mentioned at the top of this program. + file_path = os.path.dirname(os.path.abspath(__file__)) + os.chdir(file_path) + + # Variables that will hold sprite lists + self.player_list = None + self.wall_list = None + self.enemy_list = None + + # Set up the player info + self.player = None + + # Track the current state of what key is pressed + self.left_pressed = False + self.right_pressed = False + self.up_pressed = False + self.down_pressed = False + + self.physics_engine = None + + # Used in scrolling + self.view_bottom = 0 + self.view_left = 0 + + # Set the background color + arcade.set_background_color(arcade.color.AMAZON) + + def setup(self): + """ Set up the game and initialize the variables. """ + + # Sprite lists + self.player_list = arcade.SpriteList() + self.wall_list = arcade.SpriteList(use_spatial_hash=True) + self.enemy_list = arcade.SpriteList() + + # Set up the player + self.player = Player(":resources:images/animated_characters/female_person/femalePerson_idle.png", SPRITE_SCALING) + self.player.center_x = 50 + self.player.center_y = 350 + self.player_list.append(self.player) + + # Set enemies + enemy = Player(":resources:images/animated_characters/zombie/zombie_idle.png", SPRITE_SCALING) + enemy.center_x = 350 + enemy.center_y = 350 + self.enemy_list.append(enemy) + + spacing = 200 + for column in range(10): + for row in range(10): + sprite = arcade.Sprite(":resources:images/tiles/grassCenter.png", 0.5) + + x = (column + 1) * spacing + y = (row + 1) * sprite.height + + sprite.center_x = x + sprite.center_y = y + if random.randrange(100) > 20: + self.wall_list.append(sprite) + + self.physics_engine = arcade.PhysicsEngineSimple(self.player, + self.wall_list) + + def on_draw(self): + """ + Render the screen. + """ + try: + # This command has to happen before we start drawing + arcade.start_render() + + # Draw all the sprites. + self.player_list.draw() + self.wall_list.draw() + self.enemy_list.draw() + + for enemy in self.enemy_list: + if arcade.has_line_of_sight(self.player.position, + enemy.position, + self.wall_list): + color = arcade.color.RED + else: + color = arcade.color.WHITE + arcade.draw_line(self.player.center_x, + self.player.center_y, + enemy.center_x, + enemy.center_y, + color, + 2) + + except Exception as e: + print(e) + + def on_update(self, delta_time): + """ Movement and game logic """ + + # Calculate speed based on the keys pressed + self.player.change_x = 0 + self.player.change_y = 0 + + if self.up_pressed and not self.down_pressed: + self.player.change_y = MOVEMENT_SPEED + elif self.down_pressed and not self.up_pressed: + self.player.change_y = -MOVEMENT_SPEED + if self.left_pressed and not self.right_pressed: + self.player.change_x = -MOVEMENT_SPEED + elif self.right_pressed and not self.left_pressed: + self.player.change_x = MOVEMENT_SPEED + + self.physics_engine.update() + + # --- Manage Scrolling --- + + # Keep track of if we changed the boundary. We don't want to call the + # set_viewport command if we didn't change the view port. + changed = False + + # Scroll left + left_boundary = self.view_left + VIEWPORT_MARGIN + if self.player.left < left_boundary: + self.view_left -= left_boundary - self.player.left + changed = True + + # Scroll right + right_boundary = self.view_left + SCREEN_WIDTH - VIEWPORT_MARGIN + if self.player.right > right_boundary: + self.view_left += self.player.right - right_boundary + changed = True + + # Scroll up + top_boundary = self.view_bottom + SCREEN_HEIGHT - VIEWPORT_MARGIN + if self.player.top > top_boundary: + self.view_bottom += self.player.top - top_boundary + changed = True + + # Scroll down + bottom_boundary = self.view_bottom + VIEWPORT_MARGIN + if self.player.bottom < bottom_boundary: + self.view_bottom -= bottom_boundary - self.player.bottom + changed = True + + # Make sure our boundaries are integer values. While the view port does + # support floating point numbers, for this application we want every pixel + # in the view port to map directly onto a pixel on the screen. We don't want + # any rounding errors. + self.view_left = int(self.view_left) + self.view_bottom = int(self.view_bottom) + + # If we changed the boundary values, update the view port to match + if changed: + arcade.set_viewport(self.view_left, + SCREEN_WIDTH + self.view_left, + self.view_bottom, + SCREEN_HEIGHT + self.view_bottom) + + def on_key_press(self, key, modifiers): + """Called whenever a key is pressed. """ + + if key == arcade.key.UP: + self.up_pressed = True + elif key == arcade.key.DOWN: + self.down_pressed = True + elif key == arcade.key.LEFT: + self.left_pressed = True + elif key == arcade.key.RIGHT: + self.right_pressed = True + + def on_key_release(self, key, modifiers): + """Called when the user releases a key. """ + + if key == arcade.key.UP: + self.up_pressed = False + elif key == arcade.key.DOWN: + self.down_pressed = False + elif key == arcade.key.LEFT: + self.left_pressed = False + elif key == arcade.key.RIGHT: + self.right_pressed = False + + +def main(): + """ Main method """ + window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) + window.setup() + arcade.run() + + +if __name__ == "__main__": + main() diff --git a/arcade/examples/sprite_move_scrolling.py b/arcade/examples/sprite_move_scrolling.py index 5a55ce012..b1cc37faa 100644 --- a/arcade/examples/sprite_move_scrolling.py +++ b/arcade/examples/sprite_move_scrolling.py @@ -44,12 +44,16 @@ def __init__(self, width, height, title): # Sprite lists self.player_list = None - self.coin_list = None # Set up the player self.player_sprite = None + + self.coin_list = None self.wall_list = None + self.physics_engine = None + + # Used in scrolling self.view_bottom = 0 self.view_left = 0 diff --git a/arcade/paths.py b/arcade/paths.py new file mode 100644 index 000000000..e45727a4d --- /dev/null +++ b/arcade/paths.py @@ -0,0 +1,41 @@ +""" +Path-related functions. + +""" +from arcade import Point +from arcade import SpriteList +from arcade import get_distance +from arcade import lerp_vec +from arcade import get_sprites_at_point + +def has_line_of_sight(point_1: Point, + point_2: Point, + walls: SpriteList, + max_distance: int = -1, + check_resolution: int = 2): + """ + Determine if we have line of sight between two points. Try to make sure + that spatial hashing is enabled on the wall SpriteList or this will be + very slow. + + :param Point point_1: Start position + :param Point point_2: End position position + :param SpriteList walls: List of all blocking sprites + :param int max_distance: Max distance point 1 can see + :param int check_resolution: Check every x pixels for a sprite. Trade-off + between accuracy and speed. + """ + distance = get_distance(point_1[0], point_1[1], + point_2[0], point_2[1]) + steps = int(distance // check_resolution) + for step in range(steps + 1): + step_distance = step * check_resolution + u = step_distance / distance + midpoint = lerp_vec(point_1, point_2, u) + if max_distance != -1 and step_distance > max_distance: + return False + # print(point_1, point_2, step, u, step_distance, midpoint) + sprite_list = get_sprites_at_point(midpoint, walls) + if len(sprite_list) > 0: + return False + return True diff --git a/doc/examples/index.rst b/doc/examples/index.rst index e925e28cd..58366ab8c 100644 --- a/doc/examples/index.rst +++ b/doc/examples/index.rst @@ -353,6 +353,13 @@ Basic Platformers :ref:`platformer_tutorial` +.. figure:: thumbs/line_of_sight.png + :figwidth: 170px + + :ref:`line_of_sight` + + + Using Tiled Map Editor to Create Maps ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/examples/line_of_sight.png b/doc/examples/line_of_sight.png new file mode 100644 index 000000000..5e4d26a7d Binary files /dev/null and b/doc/examples/line_of_sight.png differ diff --git a/doc/examples/line_of_sight.rst b/doc/examples/line_of_sight.rst new file mode 100644 index 000000000..d976ed089 --- /dev/null +++ b/doc/examples/line_of_sight.rst @@ -0,0 +1,15 @@ +:orphan: + +.. _line_of_sight: + +Line of Sight +================================== + +.. image:: line_of_sight.png + :width: 600px + :align: center + :alt: Line of Sight + +.. literalinclude:: ../../arcade/examples/line_of_sight.py + :caption: line_of_sight.py + :linenos: diff --git a/doc/release_notes.rst b/doc/release_notes.rst index a7d5e51dc..cab05ffb7 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -26,6 +26,9 @@ Version 2.4 has: * Physics engine is less likely to 'glitch' out * Antialiasing should now work on windows if ``antialiasing=True`` is passed in the window constructor. +* Added `arcade.get_display_size` to get resolution of the monitor +* Added `Window.center_window()` to center the window on the monitor. +* Add support for `has_line_of_sight()` Version 2.3.15 -------------- diff --git a/issue_630_2/sounds/warpout.ogg.mp3 b/issue_630_2/sounds/warpout.ogg.mp3 new file mode 100644 index 000000000..9b14c5d04 Binary files /dev/null and b/issue_630_2/sounds/warpout.ogg.mp3 differ diff --git a/tests/unit2/test_drawing_primitives.py b/tests/unit2/test_drawing_primitives.py index 03b4ebb4b..ddf6d8b50 100644 --- a/tests/unit2/test_drawing_primitives.py +++ b/tests/unit2/test_drawing_primitives.py @@ -176,5 +176,6 @@ def on_draw(self): def test_main(): """ Main method """ window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT) + window.center_window() window.test() window.close() diff --git a/tests/unit2/test_line_of_sight.py b/tests/unit2/test_line_of_sight.py new file mode 100644 index 000000000..42f8c4b7a --- /dev/null +++ b/tests/unit2/test_line_of_sight.py @@ -0,0 +1,41 @@ +import arcade + +def test_line_of_sight(): + player = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png") + player.center_x = 50 + player.center_y = 350 + + enemy = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png") + enemy.center_x = 150 + enemy.center_y = 350 + + wall_list = arcade.SpriteList(use_spatial_hash=True) + + result = arcade.has_line_of_sight(player.position, enemy.position, wall_list) + assert result + + result = arcade.has_line_of_sight(player.position, enemy.position, wall_list, 2000) + assert result + + result = arcade.has_line_of_sight(player.position, enemy.position, wall_list, 20) + assert not result + + wall = arcade.Sprite(":resources:images/tiles/grassCenter.png") + wall.center_x = 0 + wall.center_y = 0 + wall_list.append(wall) + + result = arcade.has_line_of_sight(player.position, enemy.position, wall_list) + assert result + + wall.center_x = 100 + wall.center_y = 350 + + result = arcade.has_line_of_sight(player.position, enemy.position, wall_list) + assert not result + + wall.center_x = 100 + wall.center_y = 450 + + result = arcade.has_line_of_sight(player.position, enemy.position, wall_list) + assert result diff --git a/util/update_init.py b/util/update_init.py index 31a492007..95313dc59 100644 --- a/util/update_init.py +++ b/util/update_init.py @@ -74,7 +74,8 @@ def main(): "text.py", \ "tilemap.py", \ "utils.py", \ - "version.py" + "version.py", \ + "paths.py" all_list = []