519 lines
16 KiB
Python
519 lines
16 KiB
Python
from __future__ import annotations
|
|
|
|
import math
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
|
|
from Box2D import b2PolygonShape, b2Vec2
|
|
import pyray as rl
|
|
|
|
from engine.framework import GameObject
|
|
from engine.math_extensions import v2, vec_sub
|
|
from engine.prefabs.components import BodyComponent, PlatformerMovementComponent, PlatformerMovementParams, SpriteComponent
|
|
from engine.prefabs.services import PhysicsService
|
|
|
|
|
|
class StaticBox(GameObject):
|
|
"""Simple static box collider with optional debug drawing."""
|
|
def __init__(self, x: float, y: float, width: float, height: float) -> None:
|
|
""" init .
|
|
|
|
Args:
|
|
x: Parameter.
|
|
y: Parameter.
|
|
width: Parameter.
|
|
height: Parameter.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
super().__init__()
|
|
self.x = x
|
|
self.y = y
|
|
self.width = width
|
|
self.height = height
|
|
self.body = None
|
|
self.is_visible = True
|
|
|
|
@classmethod
|
|
def from_vectors(cls, position: rl.Vector2, size: rl.Vector2):
|
|
"""From vectors.
|
|
|
|
Args:
|
|
position: Parameter.
|
|
size: Parameter.
|
|
|
|
Returns:
|
|
Result of the operation.
|
|
"""
|
|
return cls(position.x, position.y, size.x, size.y)
|
|
|
|
def init(self) -> None:
|
|
"""Initialize the object.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
physics = self.scene.get_service(PhysicsService)
|
|
world = physics.world
|
|
if not world:
|
|
return
|
|
self.body = world.CreateStaticBody(position=(self.x * physics.pixels_to_meters, self.y * physics.pixels_to_meters))
|
|
shape = b2PolygonShape(box=(self.width / 2.0 * physics.pixels_to_meters,
|
|
self.height / 2.0 * physics.pixels_to_meters))
|
|
self.body.CreateFixture(shape=shape)
|
|
self.add_component(BodyComponent(self.body))
|
|
|
|
def draw(self) -> None:
|
|
"""Draw the object.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if self.is_visible:
|
|
rl.draw_rectangle(int(self.x - self.width / 2.0), int(self.y - self.height / 2.0), int(self.width), int(self.height), rl.Color(0, 121, 241, 255))
|
|
|
|
|
|
class DynamicBox(GameObject):
|
|
"""Simple dynamic rigid body box."""
|
|
def __init__(self, x: float, y: float, width: float, height: float, rotation: float = 0.0) -> None:
|
|
""" init .
|
|
|
|
Args:
|
|
x: Parameter.
|
|
y: Parameter.
|
|
width: Parameter.
|
|
height: Parameter.
|
|
rotation: Parameter.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
super().__init__()
|
|
self.x = x
|
|
self.y = y
|
|
self.width = width
|
|
self.height = height
|
|
self.rot_deg = rotation
|
|
self.physics: Optional[PhysicsService] = None
|
|
self.body = None
|
|
|
|
@classmethod
|
|
def from_vectors(cls, position: rl.Vector2, size: rl.Vector2, rotation: float = 0.0):
|
|
"""From vectors.
|
|
|
|
Args:
|
|
position: Parameter.
|
|
size: Parameter.
|
|
rotation: Parameter.
|
|
|
|
Returns:
|
|
Result of the operation.
|
|
"""
|
|
return cls(position.x, position.y, size.x, size.y, rotation)
|
|
|
|
def init(self) -> None:
|
|
"""Initialize the object.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
self.physics = self.scene.get_service(PhysicsService)
|
|
world = self.physics.world
|
|
if not world:
|
|
return
|
|
self.body = world.CreateDynamicBody(position=(self.x * self.physics.pixels_to_meters,
|
|
self.y * self.physics.pixels_to_meters),
|
|
angle=math.radians(self.rot_deg))
|
|
shape = b2PolygonShape(box=(self.width / 2.0 * self.physics.pixels_to_meters,
|
|
self.height / 2.0 * self.physics.pixels_to_meters))
|
|
self.body.CreateFixture(shape=shape, density=1.0, friction=0.3)
|
|
body_component = self.add_component(BodyComponent(self.body))
|
|
self.add_component(SpriteComponent("assets/character_green_idle.png", body_component))
|
|
|
|
def draw(self) -> None:
|
|
"""Draw the object.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not self.physics or not self.body:
|
|
return
|
|
pos = self.body.position
|
|
angle = math.degrees(self.body.angle)
|
|
rl.draw_rectangle_pro(rl.Rectangle(self.physics.convert_length_to_pixels(pos.x),
|
|
self.physics.convert_length_to_pixels(pos.y),
|
|
self.width, self.height),
|
|
v2(self.width / 2.0, self.height / 2.0),
|
|
angle,
|
|
rl.Color(230, 41, 55, 255))
|
|
|
|
|
|
class CameraObject(GameObject):
|
|
"""2D camera that follows a target with deadzone and clamp."""
|
|
def __init__(self,
|
|
size: rl.Vector2,
|
|
level_size: rl.Vector2 = v2(0.0, 0.0),
|
|
follow_speed: rl.Vector2 = v2(1000.0, 1000.0),
|
|
offset_left: float = 70.0,
|
|
offset_right: float = 70.0,
|
|
offset_top: float = 40.0,
|
|
offset_bottom: float = 40.0) -> None:
|
|
super().__init__()
|
|
|
|
self.camera = rl.Camera2D()
|
|
self.target = v2(0.0, 0.0)
|
|
self.size = size
|
|
self.level_size = level_size
|
|
self.follow_speed = follow_speed
|
|
self.offset_left = offset_left
|
|
self.offset_right = offset_right
|
|
self.offset_top = offset_top
|
|
self.offset_bottom = offset_bottom
|
|
|
|
def init(self) -> None:
|
|
"""Initialize the object.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
self.camera.zoom = 1.0
|
|
self.camera.offset = v2(self.size.x / 2.0, self.size.y / 2.0)
|
|
self.camera.rotation = 0.0
|
|
self.camera.target = self.target
|
|
|
|
def update(self, delta_time: float) -> None:
|
|
"""Update the object.
|
|
|
|
Args:
|
|
delta_time: Parameter.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
desired = self.camera.target
|
|
inv_zoom = 1.0 / self.camera.zoom if self.camera.zoom != 0.0 else 1.0
|
|
dz_left_w = self.offset_left * inv_zoom
|
|
dz_right_w = self.offset_right * inv_zoom
|
|
dz_top_w = self.offset_top * inv_zoom
|
|
dz_bottom_w = self.offset_bottom * inv_zoom
|
|
|
|
dx = self.target.x - self.camera.target.x
|
|
dy = self.target.y - self.camera.target.y
|
|
|
|
if dx < -dz_left_w:
|
|
desired.x = self.target.x + dz_left_w
|
|
elif dx > dz_right_w:
|
|
desired.x = self.target.x - dz_right_w
|
|
|
|
if dy < -dz_top_w:
|
|
desired.y = self.target.y + dz_top_w
|
|
elif dy > dz_bottom_w:
|
|
desired.y = self.target.y - dz_bottom_w
|
|
|
|
if self.follow_speed.x < 0:
|
|
self.camera.target.x = desired.x
|
|
else:
|
|
self.camera.target.x = self.move_towards(self.camera.target.x, desired.x, self.follow_speed.x * delta_time)
|
|
|
|
if self.follow_speed.y < 0:
|
|
self.camera.target.y = desired.y
|
|
else:
|
|
self.camera.target.y = self.move_towards(self.camera.target.y, desired.y, self.follow_speed.y * delta_time)
|
|
|
|
half_view = v2(self.size.x / 2.0 * inv_zoom, self.size.y / 2.0 * inv_zoom)
|
|
if self.level_size.x > self.size.x:
|
|
self.camera.target.x = max(half_view.x, min(self.level_size.x - half_view.x, self.camera.target.x))
|
|
if self.level_size.y > self.size.y:
|
|
self.camera.target.y = max(half_view.y, min(self.level_size.y - half_view.y, self.camera.target.y))
|
|
|
|
@staticmethod
|
|
def move_towards(current: float, target: float, max_delta: float) -> float:
|
|
"""Move towards.
|
|
|
|
Args:
|
|
current: Parameter.
|
|
target: Parameter.
|
|
max_delta: Parameter.
|
|
|
|
Returns:
|
|
Result of the operation.
|
|
"""
|
|
delta = target - current
|
|
if delta > max_delta:
|
|
return current + max_delta
|
|
if delta < -max_delta:
|
|
return current - max_delta
|
|
return target
|
|
|
|
def set_target(self, target: rl.Vector2) -> None:
|
|
"""Set target.
|
|
|
|
Args:
|
|
target: Parameter.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
self.target = target
|
|
|
|
def set_zoom(self, zoom: float) -> None:
|
|
"""Set zoom.
|
|
|
|
Args:
|
|
zoom: Parameter.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
self.camera.zoom = zoom
|
|
|
|
def set_rotation(self, angle: float) -> None:
|
|
"""Set rotation.
|
|
|
|
Args:
|
|
angle: Parameter.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
self.camera.rotation = angle
|
|
|
|
def draw_begin(self) -> None:
|
|
"""Draw begin.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
rl.begin_mode_2d(self.camera)
|
|
|
|
def draw_end(self) -> None:
|
|
"""Draw end.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
rl.end_mode_2d()
|
|
|
|
def draw_debug(self, color: rl.Color = rl.Color(0, 255, 0, 120)) -> None:
|
|
"""TODO"""
|
|
inv_zoom = 1.0 / self.camera.zoom if self.camera.zoom != 0.0 else 1.0
|
|
dz_left_w = self.offset_left * inv_zoom
|
|
dz_right_w = self.offset_right * inv_zoom
|
|
dz_top_w = self.offset_top * inv_zoom
|
|
dz_bottom_w = self.offset_bottom * inv_zoom
|
|
rect = rl.Rectangle(self.camera.target.x - dz_left_w,
|
|
self.camera.target.y - dz_top_w,
|
|
dz_left_w + dz_right_w,
|
|
dz_top_w + dz_bottom_w)
|
|
rl.draw_rectangle_lines_ex(rect, 2.0 * inv_zoom, color)
|
|
|
|
def screen_to_world(self, point: rl.Vector2) -> rl.Vector2:
|
|
"""Convert screen coordinates to world coordinates.
|
|
|
|
Args:
|
|
point: Parameter.
|
|
|
|
Returns:
|
|
Result of the operation.
|
|
"""
|
|
return rl.get_screen_to_world_2d(point, self.camera)
|
|
|
|
|
|
class SplitCamera(CameraObject):
|
|
"""Split-screen camera that renders to a texture."""
|
|
def __init__(self, size: rl.Vector2, level_size: rl.Vector2 = v2(0.0, 0.0),
|
|
follow_speed: rl.Vector2 = v2(1000.0, 1000.0),
|
|
offset_left: float = 70.0, offset_right: float = 70.0,
|
|
offset_top: float = 40.0, offset_bottom: float = 40.0) -> None:
|
|
super().__init__(size, level_size, follow_speed, offset_left, offset_right, offset_top, offset_bottom)
|
|
self.renderer: Optional[rl.RenderTexture] = None
|
|
|
|
def init(self) -> None:
|
|
"""Initialize the object.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
self.renderer = rl.load_render_texture(int(self.size.x), int(self.size.y))
|
|
super().init()
|
|
|
|
def draw_begin(self) -> None:
|
|
"""Draw begin.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not self.renderer:
|
|
return
|
|
rl.begin_texture_mode(self.renderer)
|
|
rl.clear_background(rl.WHITE)
|
|
rl.begin_mode_2d(self.camera)
|
|
|
|
def draw_end(self) -> None:
|
|
"""Draw end.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
rl.end_mode_2d()
|
|
rl.end_texture_mode()
|
|
|
|
def draw_texture(self, x: float, y: float) -> None:
|
|
"""Draw texture.
|
|
|
|
Args:
|
|
x: Parameter.
|
|
y: Parameter.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not self.renderer:
|
|
return
|
|
rl.draw_texture_pro(self.renderer.texture,
|
|
rl.Rectangle(0.0, 0.0, float(self.renderer.texture.width), -float(self.renderer.texture.height)),
|
|
rl.Rectangle(x, y, float(self.renderer.texture.width), float(self.renderer.texture.height)),
|
|
v2(0.0, 0.0),
|
|
0.0,
|
|
rl.WHITE)
|
|
|
|
def draw_texture_pro(self, x: float, y: float, width: float, height: float) -> None:
|
|
"""Draw texture pro.
|
|
|
|
Args:
|
|
x: Parameter.
|
|
y: Parameter.
|
|
width: Parameter.
|
|
height: Parameter.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not self.renderer:
|
|
return
|
|
rl.draw_texture_pro(self.renderer.texture,
|
|
rl.Rectangle(0.0, 0.0, float(self.renderer.texture.width), -float(self.renderer.texture.height)),
|
|
rl.Rectangle(x, y, width, height),
|
|
v2(0.0, 0.0),
|
|
0.0,
|
|
rl.WHITE)
|
|
|
|
def screen_to_world_with_offset(self, draw_position: rl.Vector2, point: rl.Vector2) -> rl.Vector2:
|
|
"""Convert screen coordinates to world coordinates.
|
|
|
|
Args:
|
|
draw_position: Parameter.
|
|
point: Parameter.
|
|
|
|
Returns:
|
|
Result of the operation.
|
|
"""
|
|
local_point = vec_sub(point, draw_position)
|
|
return rl.get_screen_to_world_2d(local_point, self.camera)
|
|
|
|
|
|
@dataclass
|
|
class CharacterParams:
|
|
"""Parameter bag for a platformer character."""
|
|
width: float = 24.0
|
|
height: float = 40.0
|
|
position: rl.Vector2 = v2(0.0, 0.0)
|
|
friction: float = 0.0
|
|
restitution: float = 0.0
|
|
density: float = 1.0
|
|
|
|
|
|
class PlatformerCharacter(GameObject):
|
|
"""Simple platformer character with movement."""
|
|
def __init__(self, params: CharacterParams, gamepad: int = 0) -> None:
|
|
""" init .
|
|
|
|
Args:
|
|
params: Parameter.
|
|
gamepad: Parameter.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
super().__init__()
|
|
self.p = params
|
|
self.physics: Optional[PhysicsService] = None
|
|
self.body: Optional[BodyComponent] = None
|
|
self.movement: Optional[PlatformerMovementComponent] = None
|
|
self.gamepad = gamepad
|
|
|
|
def init(self) -> None:
|
|
"""Initialize the object.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
self.physics = self.scene.get_service(PhysicsService)
|
|
|
|
def build_body(component: BodyComponent):
|
|
"""Build body.
|
|
|
|
Args:
|
|
component: Parameter.
|
|
|
|
Returns:
|
|
Result of the operation.
|
|
"""
|
|
world = self.physics.world
|
|
body = world.CreateDynamicBody(position=(self.physics.convert_to_meters(self.p.position).x,
|
|
self.physics.convert_to_meters(self.p.position).y),
|
|
fixedRotation=True)
|
|
body.userData = self
|
|
shape = b2PolygonShape(box=(self.physics.convert_length_to_meters(self.p.width / 2.0),
|
|
self.physics.convert_length_to_meters(self.p.height / 2.0)))
|
|
body.CreateFixture(shape=shape, density=self.p.density, friction=self.p.friction, restitution=self.p.restitution)
|
|
component.body = body
|
|
|
|
self.body = self.add_component(BodyComponent(build=build_body))
|
|
params = PlatformerMovementParams()
|
|
params.width = self.p.width
|
|
params.height = self.p.height
|
|
self.movement = self.add_component(PlatformerMovementComponent(params))
|
|
|
|
def update(self, delta_time: float) -> None:
|
|
|
|
"""Update the object.
|
|
|
|
Args:
|
|
delta_time: Parameter.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
deadzone = 0.1
|
|
jump_pressed = rl.is_key_pressed(rl.KEY_W) or rl.is_gamepad_button_pressed(self.gamepad, rl.GAMEPAD_BUTTON_RIGHT_FACE_DOWN)
|
|
jump_held = rl.is_key_down(rl.KEY_W) or rl.is_gamepad_button_down(self.gamepad, rl.GAMEPAD_BUTTON_RIGHT_FACE_DOWN)
|
|
|
|
move_x = rl.get_gamepad_axis_movement(self.gamepad, rl.GAMEPAD_AXIS_LEFT_X)
|
|
if abs(move_x) < deadzone:
|
|
move_x = 0.0
|
|
if rl.is_key_down(rl.KEY_D) or rl.is_gamepad_button_down(self.gamepad, rl.GAMEPAD_BUTTON_LEFT_FACE_RIGHT):
|
|
move_x = 1.0
|
|
elif rl.is_key_down(rl.KEY_A) or rl.is_gamepad_button_down(self.gamepad, rl.GAMEPAD_BUTTON_LEFT_FACE_LEFT):
|
|
move_x = -1.0
|
|
|
|
if self.movement:
|
|
self.movement.set_input(move_x, jump_pressed, jump_held)
|
|
|
|
def draw(self) -> None:
|
|
|
|
"""Draw the object.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not self.body or not self.movement:
|
|
return
|
|
color = rl.GREEN if self.movement.grounded else rl.BLUE
|
|
pos = self.body.get_position_pixels()
|
|
rl.draw_rectangle_pro(rl.Rectangle(pos.x, pos.y, self.p.width, self.p.height),
|
|
v2(self.p.width / 2.0, self.p.height / 2.0),
|
|
0.0,
|
|
color)
|