From 96b30a7ba3904a54789557d8a2aa4b88af72756b Mon Sep 17 00:00:00 2001 From: Tomer Sedan Date: Sun, 28 Aug 2022 22:30:58 -0400 Subject: [PATCH] Asteroids demo (#333) * wahoo first step of asteroids done * add basic player * optimize within * physics fps to 50 and fix demos * controller in asteroids * auto-normalize controller joysticks * remove docstring * keyboard support * shooting * shooting but capped * shots delete asteroids --- CHANGELOG.md | 4 + demo/asteroids.py | 163 +++++++++++++++++++++ demo/offset_demo.py | 4 +- demo/physics_demo.py | 24 +-- demo/platformer.py | 48 +++--- rubato/game.py | 2 +- rubato/struct/gameobject/physics/engine.py | 3 +- rubato/struct/gameobject/sprites/raster.py | 31 ++-- rubato/struct/surface.py | 9 ++ rubato/utils/radio.py | 25 ++-- rubato/utils/rb_input.py | 21 +-- rubato/utils/rb_time.py | 4 +- rubato/utils/vector.py | 34 +++-- 13 files changed, 288 insertions(+), 84 deletions(-) create mode 100644 demo/asteroids.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ee8908b1..d9e971766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ - `GameObject`s and `Group`s can now be hidden in order to make their children not draw. - `GameObjects`s can be active just like groups. - Allowing passing in the hidden attribute into `Component` constructors. +- `Raster.fill()` and `Surface.fill()`. +- `Vector.within()` method to check if vector is within a certain distance of another vector. - `Raster` component and `Surface` now have a changeable alpha. (`Image` and `Sprite` by extension) ### Changed @@ -35,6 +37,8 @@ - Rewrote `Rectangle` from the ground-up. - Window is now shown when begin is called. Not when init is called. - `mouse_button` key passed in mouse press events renamed to `button` +- Default physics fps to 50 to align with Unity. +- Automatically normalized joystick events/getters to be in the range of -1 to 1 instead of -32768 to 32767. ### Removed diff --git a/demo/asteroids.py b/demo/asteroids.py new file mode 100644 index 000000000..300b0c868 --- /dev/null +++ b/demo/asteroids.py @@ -0,0 +1,163 @@ +""" +A classic +""" +import random +from rubato import * + +size = 1080 +bounds = size // 8 +radius = size // 40 +level = 0 + +init(name="asteroids", res=(size, size)) + +main = Scene() + +# background +stars = Surface(size, size) +stars.fill(Color.black) +for i in range(200): + pos = random.randint(0, size), random.randint(0, size) + stars.draw_point(pos, Color.white) + + +# component to remove things that are out of bounds +class BoundsChecker(Component): + + def update(self): + if self.gameobj.pos.x < -bounds or self.gameobj.pos.x > size + bounds or \ + self.gameobj.pos.y < -bounds or self.gameobj.pos.y > size + bounds: + main.delete(self.gameobj) + + +# asteroid generator +def make_asteroid(): + sides = random.randint(5, 8) + + t = random.randint(0, size) + topbottom = random.randint(0, 1) + side = random.randint(0, 1) + if topbottom: + pos = t, side * size + (radius if side else -radius) + else: + pos = side * size + (radius if side else -radius), t + + dir = (-Display.center.dir_to(pos)).rotate(random.randint(-45, 45)) + + main.add( + wrap( + [ + Polygon( + [ + Vector.from_radial(random.randint(int(radius * .7), int(radius * 0.95)), i * 360 / sides) + for i in range(sides) + ], + debug=True, + ), + RigidBody(velocity=dir * 100, ang_vel=random.randint(-30, 30)), + BoundsChecker(), + ], + "asteroid", + pos, + random.randint(0, 360), + ) + ) + + +Time.schedule(ScheduledTask(1000, make_asteroid, 1000)) + + +class PlayerController(Component): + + def setup(self): + self.speed = 200 + self.steer = 20 + + self.velocity = Vector() + + def update(self): + if Input.controller_button(Input.controllers - 1, 0) or Input.key_pressed("j"): + shoot() + + def fixed_update(self): + dx = Input.controller_axis(Input.controllers - 1, 0) or \ + (-1 if Input.key_pressed("a") else (1 if Input.key_pressed("d") else 0)) + dy = Input.controller_axis(Input.controllers - 1, 1) or \ + (-1 if Input.key_pressed("w") else (1 if Input.key_pressed("s") else 0)) + target = Vector(dx, dy) + + d_vel = target * self.speed + steering = Vector.clamp_magnitude(d_vel - self.velocity, self.steer) + + self.velocity = Vector.clamp_magnitude(self.velocity + steering, self.speed) + + self.gameobj.pos += self.velocity * Time.fixed_delta + + if target != (0, 0): + self.gameobj.rotation = self.velocity.angle + + +full = [ + Vector.from_radial(radius, 0), + Vector.from_radial(radius, 125), + Vector.from_radial(radius // 4, 180), + Vector.from_radial(radius, -125), +] +right = [full[0], full[1], full[2]] +left = [full[0], full[2], full[3]] +player_spr = Raster(radius * 2, radius * 2) +player_spr.draw_poly([v + radius for v in full], Color.debug, 2, aa=True) + +main.add( + wrap( + [ + PlayerController(), + Polygon(right, trigger=True), + Polygon(left, trigger=True), + player_spr, + ], + "player", + Display.center, + ) +) + +last_shoot = 0 +interval = 200 # milliseconds between shots + + +def bullet_collide(man: Manifold): + if man.shape_b.gameobj.name == "asteroid": + main.delete(man.shape_b.gameobj) + + +def shoot(): + global last_shoot + if Time.now() - last_shoot < interval: + return + last_shoot = Time.now() + main.add( + wrap( + [ + Circle(radius // 5, Color.debug, trigger=True, on_collide=bullet_collide), + RigidBody( + velocity=player_spr.gameobj.get(PlayerController).velocity + Vector.from_radial( + 500, + player_spr.gameobj.rotation, + ) + ), + BoundsChecker(), + ], + "bullet", + player_spr.gameobj.pos + full[0].rotate(player_spr.gameobj.rotation), + player_spr.gameobj.rotation, + ) + ) + + +def new_draw(): + Draw.surf(stars, Display.center) + + +Game.draw = new_draw + +begin() diff --git a/demo/offset_demo.py b/demo/offset_demo.py index d988a7b03..613cb109b 100644 --- a/demo/offset_demo.py +++ b/demo/offset_demo.py @@ -28,7 +28,7 @@ def update(): def handler(m_event): - if m_event["mouse_button"] == "mouse 1": + if m_event["button"] == "mouse 1": e = extra.clone() e.pos = V(m_event["x"], m_event["y"]) s.add(e) @@ -39,4 +39,4 @@ def handler(m_event): s.add(go, rb.wrap(text, pos=V(50, 20))) s.fixed_update = update -rb.begin() \ No newline at end of file +rb.begin() diff --git a/demo/physics_demo.py b/demo/physics_demo.py index eaee2e7b9..80cc270f1 100644 --- a/demo/physics_demo.py +++ b/demo/physics_demo.py @@ -59,16 +59,16 @@ rb.Circle(radius=rb.Display.res.x // num_obj, color=rb.Color.random_default()), rb.RigidBody( mass=0.1, - bounciness=1, + bounciness=0.99, friction=0.2, - gravity=(0, 50), + gravity=(0, 80), velocity=(randint(-100, 100), randint(-100, 100)), - ) + ), ], pos=( - randint(rb.Display.res.x / 20, - 19 * rb.Display.res.x / 20), randint(rb.Display.res.y / 20, 19 * rb.Display.res.y / 20) - ) + randint(rb.Display.res.x / 20, 19 * rb.Display.res.x / 20), + randint(rb.Display.res.y / 20, 19 * rb.Display.res.y / 20), + ), ) ) for _ in range(num_obj // 2): @@ -78,16 +78,16 @@ rb.Polygon(rb.Vector.poly(randint(3, 9), rb.Display.res.x // num_obj), color=rb.Color.random_default()), rb.RigidBody( mass=0.1, - bounciness=1, + bounciness=0.99, friction=0.2, - gravity=(0, 50), + gravity=(0, 80), velocity=(randint(-100, 100), randint(-100, 100)), - ) + ), ], pos=( - randint(rb.Display.res.x / 20, - 19 * rb.Display.res.x / 20), randint(rb.Display.res.y / 20, 19 * rb.Display.res.y / 20) - ) + randint(rb.Display.res.x / 20, 19 * rb.Display.res.x / 20), + randint(rb.Display.res.y / 20, 19 * rb.Display.res.y / 20), + ), ) ) diff --git a/demo/platformer.py b/demo/platformer.py index da8cb084a..3f8f6eaf9 100644 --- a/demo/platformer.py +++ b/demo/platformer.py @@ -96,20 +96,24 @@ def re_allow(): # create platforms platforms = [ - rb.GameObject(pos=rb.Vector(200, rb.Display.bottom - 140) - ).add(rb.Rectangle( - width=90, - height=40, - tag="ground", - color=rb.Color.blue, - )), - rb.GameObject(pos=rb.Vector(400, rb.Display.bottom - 340) - ).add(rb.Rectangle( - width=150, - height=40, - tag="ground", - color=rb.Color.blue, - )), + rb.GameObject(pos=rb.Vector( + 200, + rb.Display.bottom - 140, + )).add(rb.Rectangle( + width=90, + height=40, + tag="ground", + color=rb.Color.blue, + )), + rb.GameObject(pos=rb.Vector( + 400, + rb.Display.bottom - 340, + )).add(rb.Rectangle( + width=150, + height=40, + tag="ground", + color=rb.Color.blue, + )), ] # create obstacles @@ -133,13 +137,15 @@ def re_allow(): # create triggers triggers = [ - rb.GameObject(pos=rb.Vector(950, rb.Display.bottom - - 45),).add(rb.Rectangle( - width=400, - height=30, - tag="retry_collider", - trigger=True, - )), + rb.GameObject(pos=rb.Vector( + 950, + rb.Display.bottom - 45, + )).add(rb.Rectangle( + width=400, + height=30, + tag="retry_collider", + trigger=True, + )), ] # Create animation for portal diff --git a/rubato/game.py b/rubato/game.py index 12dda288f..1223a96ff 100644 --- a/rubato/game.py +++ b/rubato/game.py @@ -205,5 +205,5 @@ def update(): # test: skip @staticmethod def draw(): # test: skip - """An overrideable method for drawing the game. Called once per frame, after the current scene draws.""" + """An overrideable method for drawing the game. Called once per frame, before the draw queue is dumped.""" pass diff --git a/rubato/struct/gameobject/physics/engine.py b/rubato/struct/gameobject/physics/engine.py index cec268711..7f3ea9d77 100644 --- a/rubato/struct/gameobject/physics/engine.py +++ b/rubato/struct/gameobject/physics/engine.py @@ -371,7 +371,8 @@ def __init__( """The direction that would most quickly separate the two colliders.""" def __str__(self) -> str: - return f"Manifold <{self.penetration}, {self.normal}>" + return f"Manifold " def flip(self) -> Manifold: """ diff --git a/rubato/struct/gameobject/sprites/raster.py b/rubato/struct/gameobject/sprites/raster.py index 063e51771..6cc7afdf9 100644 --- a/rubato/struct/gameobject/sprites/raster.py +++ b/rubato/struct/gameobject/sprites/raster.py @@ -97,13 +97,22 @@ def delete(self): def clear(self): """ - Clears the image. + Clears the surface. """ self.surf.clear() + def fill(self, color: Color): + """ + Fill the surface with a color. + + Args: + color: The color to fill with. + """ + self.surf.fill(color) + def draw_point(self, pos: Vector | tuple[float, float], color: Color = Color.black, blending: bool = True): """ - Draws a point on the image. + Draws a point on the surface. Args: pos: The position to draw the point. @@ -122,7 +131,7 @@ def draw_line( blending: bool = True ): """ - Draws a line on the image. + Draws a line on the surface. Args: start: The start of the line. @@ -144,7 +153,7 @@ def draw_rect( blending: bool = True, ): """ - Draws a rectangle on the image. + Draws a rectangle on the surface. Args: top_left: The top left corner of the rectangle. @@ -167,7 +176,7 @@ def draw_circle( blending: bool = True, ): """ - Draws a circle on the image. + Draws a circle on the surface. Args: center: The center of the circle. @@ -190,7 +199,7 @@ def draw_poly( blending: bool = True, ): """ - Draws a polygon on the image. + Draws a polygon on the surface. Args: points: The points of the polygon. @@ -204,7 +213,7 @@ def draw_poly( def get_size(self) -> Vector: """ - Gets the current size of the image. + Gets the current size of the surface. Returns: The size of the surface @@ -213,7 +222,7 @@ def get_size(self) -> Vector: def get_pixel(self, pos: Vector | tuple[float, float]) -> Color: """ - Gets the color of a pixel on the image. + Gets the color of a pixel on the surface. Args: pos: The position of the pixel. @@ -225,7 +234,7 @@ def get_pixel(self, pos: Vector | tuple[float, float]) -> Color: def get_pixel_tuple(self, pos: Vector | tuple[float, float]) -> tuple[int, int, int, int]: """ - Gets the color of a pixel on the image. + Gets the color of a pixel on the surface. Args: pos: The position of the pixel. @@ -237,7 +246,7 @@ def get_pixel_tuple(self, pos: Vector | tuple[float, float]) -> tuple[int, int, def switch_color(self, color: Color, new_color: Color): """ - Switches a color in the image. + Switches a color in the surface. Args: color: The color to switch. @@ -247,7 +256,7 @@ def switch_color(self, color: Color, new_color: Color): def set_colorkey(self, color: Color): """ - Sets the colorkey of the image. + Sets the colorkey of the surface. Args: color: Color to set as the colorkey. """ diff --git a/rubato/struct/surface.py b/rubato/struct/surface.py index 392b5a100..52fec3169 100644 --- a/rubato/struct/surface.py +++ b/rubato/struct/surface.py @@ -118,6 +118,15 @@ def clear(self): c_draw.clear_pixels(self.surf.pixels, self.surf.w, self.surf.h) self.uptodate = False + def fill(self, color: Color): + """ + Fill the surface with a color. + + Args: + color: The color to fill with. + """ + self.draw_rect((0, 0), (self.surf.w, self.surf.h), fill=color) + def draw_point(self, pos: Vector | tuple[float, float], color: Color = Color.black, blending: bool = True): """ Draws a point on the surface. diff --git a/rubato/utils/radio.py b/rubato/utils/radio.py index 004b1f16b..ff7b6981f 100644 --- a/rubato/utils/radio.py +++ b/rubato/utils/radio.py @@ -116,17 +116,17 @@ def handle(cls) -> bool: }, ) elif event.type in (sdl2.SDL_MOUSEBUTTONDOWN, sdl2.SDL_MOUSEBUTTONUP): - mouse_button = None + button = None if event.button.state == sdl2.SDL_BUTTON_LEFT: - mouse_button = "mouse 1" + button = "mouse 1" elif event.button.state == sdl2.SDL_BUTTON_MIDDLE: - mouse_button = "mouse 2" + button = "mouse 2" elif event.button.state == sdl2.SDL_BUTTON_RIGHT: - mouse_button = "mouse 3" + button = "mouse 3" elif event.button.state == sdl2.SDL_BUTTON_X1: - mouse_button = "mouse 4" + button = "mouse 4" elif event.button.state == sdl2.SDL_BUTTON_X2: - mouse_button = "mouse 5" + button = "mouse 5" if event.type == sdl2.SDL_MOUSEBUTTONUP: event_name = Events.MOUSEUP @@ -136,7 +136,7 @@ def handle(cls) -> bool: cls.broadcast( event_name, { - "button": mouse_button, + "button": button, "x": event.button.x, "y": event.button.y, "clicks": event.button.clicks, @@ -160,13 +160,14 @@ def handle(cls) -> bool: }, ) elif event.type == sdl2.SDL_JOYAXISMOTION: + mag: float = event.jaxis.value / Input._joystick_max cls.broadcast( Events.JOYAXISMOTION, { "controller": event.jaxis.which, "axis": event.jaxis.axis, - "value": event.jaxis.value, - "centered": Input.axis_centered(event.jaxis.value), + "value": mag, + "centered": Input.axis_centered(mag), }, ) elif event.type == sdl2.SDL_JOYBUTTONDOWN: @@ -254,11 +255,11 @@ class Listener: event: The event key to listen for. callback: The function to run once the event is broadcast. """ - event: str = cython.declare(str, visibility="public") # type: ignore + event: str = cython.declare(str, visibility="public") # type: ignore """The event descriptor""" - callback: Callable = cython.declare(object, visibility="public") # type: ignore + callback: Callable = cython.declare(object, visibility="public") # type: ignore """The function called when the event occurs""" - registered: cython.bint = cython.declare(cython.bint, visibility="public") # type: ignore + registered: cython.bint = cython.declare(cython.bint, visibility="public") # type: ignore """Describes whether the listener is registered""" def __init__(self, event: str, callback: Callable): diff --git a/rubato/utils/rb_input.py b/rubato/utils/rb_input.py index 0f52371dd..c93e3afc2 100644 --- a/rubato/utils/rb_input.py +++ b/rubato/utils/rb_input.py @@ -19,6 +19,7 @@ class Input: # CONTROLLER METHODS _controllers: list[sdl2.SDL_Joystick] = [] + _joystick_max: int = 32768 @classmethod @property @@ -83,39 +84,39 @@ def controller_name(cls, controller: int) -> str: return sdl2.SDL_JoystickNameForIndex(controller) @classmethod - def controller_axis(cls, controller: int, axis: int) -> int: + def controller_axis(cls, controller: int, axis: int) -> float: """ Get the value of a given joystick axis on a controller. Args: - controller (int): The index of the controller. - axis (int): The index of the joystick axis. + controller: The index of the controller. + axis: The index of the joystick axis. Raises: IndexError: The given controller index is out of range. Note that no error is thrown if controller is negative. Returns: - int: The value of the axis. If controller is less than 0, returns 0. + The value of the axis. If controller is less than 0, returns 0. """ if controller < 0: return 0 if controller >= len(cls._controllers): raise IndexError(f"Index {controller} out of range.") - return sdl2.SDL_JoystickGetAxis(cls._controllers[controller], axis) + return sdl2.SDL_JoystickGetAxis(cls._controllers[controller], axis) / cls._joystick_max @classmethod - def axis_centered(cls, val: int) -> bool: + def axis_centered(cls, val: float) -> bool: """ - Check whether a given axis value is within the 10% bounds of deadzone considered the "center". + Check whether a given axis value is within the +/-10% bounds of deadzone considered the "center". Args: - val (int): The value of the axis. + val: The value of the axis. Returns: - bool: Whether the axis is centered. + Whether the axis is centered. """ - return -3200 < val < 3200 + return -0.1 < val < 0.1 @classmethod def controller_button(cls, controller: int, button: int) -> bool: diff --git a/rubato/utils/rb_time.py b/rubato/utils/rb_time.py index 3ab7a1ceb..870270b3a 100644 --- a/rubato/utils/rb_time.py +++ b/rubato/utils/rb_time.py @@ -79,8 +79,8 @@ class Time: """The fps that the game should try to run at. 0 means that the game's fps will not be capped. Defaults to 0.""" capped: bool = False - physics_fps = 30 - """The fps that the physics should run at. Defaults to 30.""" + physics_fps = 50 + """The fps that the physics should run at. Defaults to 50.""" def __init__(self) -> None: raise InitError(self) diff --git a/rubato/utils/vector.py b/rubato/utils/vector.py index ba5da0f05..e4cd30217 100644 --- a/rubato/utils/vector.py +++ b/rubato/utils/vector.py @@ -342,9 +342,9 @@ def abs(self, out: Vector | None = None) -> Vector: return out - def dir_to(self, other: Vector) -> Vector: + def dir_to(self, other: Vector | tuple[float, float]) -> Vector: """ - Direction from the Vector to another Vector. + Direction from the Vector to another Vector (or tuple of floats). Args: other: the position to which you are pointing @@ -352,20 +352,32 @@ def dir_to(self, other: Vector) -> Vector: Returns: A unit vector that is in the pointing to the other position passed in """ - base = (other - self).normalized() - return base + return (other - self).normalized() - def dist_to(self, other: Vector) -> float: + def dist_to(self, other: Vector | tuple[float, float]) -> float: """ - Finds the pythagorean distance between two vectors. + Finds the pythagorean distance to another vector (or tuple of floats). Args: - other (Vector): The other vector. + other: The other vector. Returns: - float: The distance. + The distance. + """ + return math.sqrt((self.x - other[0])**2 + (self.y - other[1])**2) + + def within(self, other: Vector | tuple[float, float], distance: float | int) -> bool: """ - return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2) + Checks if the vector is within a certain distance of another vector (or tuple of floats). + + Args: + other: The other vector + distance: The distance to check + + Returns: + True if the vector is within the distance, False otherwise + """ + return (self.x - other[0])**2 + (self.y - other[1])**2 <= distance * distance @deprecated(dist_to) def distance_between(self, other: Vector) -> float: @@ -383,9 +395,7 @@ def from_radial(magnitude: float | int, angle: float | int) -> Vector: Returns: Vector from the given direction and distance """ - radians = math.radians(-(angle - 90)) - - return Vector(round(math.cos(radians), 10) * magnitude, -round(math.sin(radians), 10) * magnitude) + return Vector.up().rotate(angle) * magnitude @staticmethod def clamp_magnitude(vector: Vector, max_magnitude: float | int, min_magnitude: float | int = 0) -> Vector: