Implemented framework for player controller and input handling

main
Yan Wittmann 2023-03-26 12:46:41 +02:00
parent da20414611
commit 65c8a17543
15 changed files with 156 additions and 92 deletions

View File

@ -1,19 +1,18 @@
import random import random
import pygame import pygame
from pygame.font import Font
from level.LevelManager import LevelManager from level.LevelManager import LevelManager
from physics.SpriteManager import SpriteManager, DrawLayers from physics.SpriteManager import SpriteManager, DrawLayers
from physics.TickData import TickData
from sprite.DynamicSprite import DynamicSprite from sprite.DynamicSprite import DynamicSprite
from sprite.PositionScale import PositionScale from sprite.PositionScale import PositionScale
from sprite.SpritesheetManager import SpritesheetManager from sprite.SpritesheetManager import SpritesheetManager
from physics.PhysicsElementsHandler import PhysicsElementsHandler
from sprite.Sprite import Sprite from sprite.Sprite import Sprite
from sprite.StaticSprite import StaticSprite from sprite.StaticSprite import StaticSprite
from ui_elements.ClickEvent import ClickEvent from ui_elements.ClickEvent import ClickEvent
from ui_elements.KeyEvent import KeyEvent
from ui_elements.TextLabel import TextLabel from ui_elements.TextLabel import TextLabel
from ui_elements.UiElement import UiElement
what_to_run = 'physics' what_to_run = 'physics'
@ -70,32 +69,19 @@ elif what_to_run == 'physics':
while True: while True:
clock.tick(frame_rate) clock.tick(frame_rate)
skip = False skip = True
for event in pygame.event.get(): pygame_events: list[pygame.event.Event] = pygame.event.get()
key_events: list[KeyEvent] = KeyEvent.create_events(pygame_events)
click_events: list[ClickEvent] = ClickEvent.create_events(pygame_events, screen_transform)
for event in pygame_events:
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
pygame.quit() pygame.quit()
quit() quit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT: for key_event in key_events:
if key_event.is_keymap_down(KeyEvent.KEY_RIGHT):
skip = False skip = False
elif event.type == pygame.MOUSEBUTTONUP:
screen_pos = pygame.mouse.get_pos()
world_pos = UiElement.transform_screen_to_world(screen_pos, screen_transform)
click_event = ClickEvent(world_pos, screen_pos, event)
if event.button == 1:
test_dyn_sprite = DynamicSprite(spritesheet_manager.get_sheet("test_1"))
test_dyn_sprite.position_scale = PositionScale(world_pos, (1, 1))
sprite_manager.add_ui_element(DrawLayers.OBJECTS, test_dyn_sprite)
elif event.button == 3:
test_dyn_sprite = StaticSprite(spritesheet_manager.get_sheet("test_1"))
test_dyn_sprite.position_scale = PositionScale(world_pos, (1, 1))
sprite_manager.add_ui_element(DrawLayers.OBJECTS, test_dyn_sprite)
elif event.button == 2:
for element in sprite_manager.find_elements_at_position(world_pos):
sprite_manager.remove_ui_element(element)
element.click(click_event)
if skip: if skip:
continue continue
@ -104,7 +90,7 @@ elif what_to_run == 'physics':
text_1.set_text(f"Frame: {counter}") text_1.set_text(f"Frame: {counter}")
screen.fill((0, 0, 0)) screen.fill((0, 0, 0))
sprite_manager.tick(apply_frame_rate(1)) sprite_manager.tick(TickData(apply_frame_rate(1), pygame_events, key_events, click_events, screen_transform))
sprite_manager.draw(screen, screen_transform) sprite_manager.draw(screen, screen_transform)
pygame.display.update() pygame.display.update()

View File

@ -1,5 +1,6 @@
from typing import Optional from typing import Optional
from physics.TickData import TickData
from sprite.DynamicSprite import DynamicSprite from sprite.DynamicSprite import DynamicSprite
from sprite.Sprite import Sprite from sprite.Sprite import Sprite
from sprite.StaticSprite import StaticSprite from sprite.StaticSprite import StaticSprite
@ -19,11 +20,11 @@ class PhysicsElementsHandler:
def remove_collision_callback(self, callback): def remove_collision_callback(self, callback):
self.collision_callbacks.remove(callback) self.collision_callbacks.remove(callback)
def tick(self, dt: float, layers: dict[str, list[UiElement]]): def tick(self, tick_data: TickData, layers: dict[str, list[UiElement]]):
sprites = [] sprites = []
for layer in layers: for layer in layers:
for sprite in layers[layer]: for sprite in layers[layer]:
sprite.tick(dt) sprite.tick(tick_data)
if isinstance(sprite, Sprite): if isinstance(sprite, Sprite):
sprites.append(sprite) sprites.append(sprite)
@ -44,7 +45,7 @@ class PhysicsElementsHandler:
sorted_dynamic_sprites = sorted(dynamic_sprites, key=lambda spr: spr.position_scale.position[1]) sorted_dynamic_sprites = sorted(dynamic_sprites, key=lambda spr: spr.position_scale.position[1])
for sprite in sorted_dynamic_sprites: for sprite in sorted_dynamic_sprites:
collides_with = self.attempt_move(dt, sprite, colliders, MOTION_STEPS) collides_with = self.attempt_move(tick_data.dt, sprite, colliders, MOTION_STEPS)
if collides_with is not None: if collides_with is not None:
for callback in self.collision_callbacks: for callback in self.collision_callbacks:
callback(sprite, collides_with) callback(sprite, collides_with)

View File

@ -1,8 +1,10 @@
from typing import Optional from typing import Optional
import pygame
from pygame import Surface from pygame import Surface
from physics.PhysicsElementsHandler import PhysicsElementsHandler from physics.PhysicsElementsHandler import PhysicsElementsHandler
from physics.TickData import TickData
from sprite.PositionScale import PositionScale from sprite.PositionScale import PositionScale
from sprite.Sprite import Sprite from sprite.Sprite import Sprite
from ui_elements.UiElement import UiElement from ui_elements.UiElement import UiElement
@ -35,8 +37,8 @@ class SpriteManager:
if sprite in self.layers[layer]: if sprite in self.layers[layer]:
self.layers[layer].remove(sprite) self.layers[layer].remove(sprite)
def tick(self, dt: float): def tick(self, tick_data: TickData):
self.physics_handler.tick(dt, self.layers) self.physics_handler.tick(tick_data, self.layers)
def draw(self, screen: Surface, screen_transform: PositionScale): def draw(self, screen: Surface, screen_transform: PositionScale):
for layer in DrawLayers.DRAW_ORDER: for layer in DrawLayers.DRAW_ORDER:

View File

@ -0,0 +1,19 @@
import pygame
from sprite.PositionScale import PositionScale
from ui_elements.ClickEvent import ClickEvent
from ui_elements.KeyEvent import KeyEvent
class TickData:
def __init__(self, dt: float, pygame_events: list[pygame.event.Event], key_events: list[KeyEvent],
click_events: list[ClickEvent], screen_transform: PositionScale):
self.dt = dt
self.pygame_events = pygame_events
self.key_events = key_events
self.click_events = click_events
self.screen_transform = screen_transform
@staticmethod
def empty():
pass

View File

@ -0,0 +1,10 @@
from ui_elements.ClickEvent import ClickEvent
from ui_elements.KeyEvent import KeyEvent
class ElementController:
def __init__(self):
pass
def handle_input(self, dt: float, key_events: list[KeyEvent], click_events: list[ClickEvent]):
pass

View File

@ -0,0 +1,5 @@
from physics.controllers.ElementController import ElementController
class PlayerController(ElementController):
pass

View File

@ -1,11 +1,6 @@
import pygame
import sys
from sprite.Sprite import Sprite
from sprite.Spritesheet import Spritesheet from sprite.Spritesheet import Spritesheet
from sprite.StaticSprite import StaticSprite from sprite.StaticSprite import StaticSprite
from physics.TickData import TickData
sys.path.append('./sprite')
class DynamicSprite(StaticSprite): class DynamicSprite(StaticSprite):
@ -21,6 +16,21 @@ class DynamicSprite(StaticSprite):
# up, right, down, left # up, right, down, left
self.touches_bounding = (False, False, False, False) self.touches_bounding = (False, False, False, False)
def tick(self, tick_data: TickData):
super().tick(tick_data)
deceleration_horizontal = 0
if abs(self.motion[0]) > 0:
if self.touches_bounding[2]:
deceleration_horizontal = self.deceleration_horizontal_ground
else:
deceleration_horizontal = self.deceleration_horizontal_air
self.motion = (
self.motion[0] - deceleration_horizontal * self.motion[0] * tick_data.dt,
self.motion[1] + self.gravity * tick_data.dt
)
def set_touches_bottom(self, value: bool): def set_touches_bottom(self, value: bool):
self.touches_bounding = (self.touches_bounding[0], self.touches_bounding[1], value, self.touches_bounding[3]) self.touches_bounding = (self.touches_bounding[0], self.touches_bounding[1], value, self.touches_bounding[3])
@ -35,18 +45,3 @@ class DynamicSprite(StaticSprite):
def reset_touches(self): def reset_touches(self):
self.touches_bounding = (False, False, False, False) self.touches_bounding = (False, False, False, False)
def tick(self, dt: float):
super().tick(dt)
deceleration_horizontal = 0
if abs(self.motion[0]) > 0:
if self.touches_bounding[2]:
deceleration_horizontal = self.deceleration_horizontal_ground
else:
deceleration_horizontal = self.deceleration_horizontal_air
self.motion = (
self.motion[0] - deceleration_horizontal * self.motion[0] * dt,
self.motion[1] + self.gravity * dt
)

View File

@ -1,13 +1,11 @@
import pygame import pygame
import sys
from physics.TickData import TickData
from sprite.BoundingBox import BoundingBox from sprite.BoundingBox import BoundingBox
from sprite.PositionScale import PositionScale from sprite.PositionScale import PositionScale
from sprite.Spritesheet import Spritesheet from sprite.Spritesheet import Spritesheet
from ui_elements.UiElement import UiElement from ui_elements.UiElement import UiElement
sys.path.append('./sprite')
class Sprite(UiElement): class Sprite(UiElement):
def __init__(self, spritesheet: Spritesheet): def __init__(self, spritesheet: Spritesheet):
@ -24,11 +22,11 @@ class Sprite(UiElement):
self.is_collider = True self.is_collider = True
def tick(self, dt: float): def tick(self, tick_data: TickData):
animation = self.spritesheet.animations[self.animation_state] animation = self.spritesheet.animations[self.animation_state]
if self.animated: if self.animated:
self.animation_delay += dt self.animation_delay += tick_data.dt
while self.animation_delay >= animation['delays'][self.animation_frame % len(animation['delays'])]: while self.animation_delay >= animation['delays'][self.animation_frame % len(animation['delays'])]:
self.animation_delay -= animation['delays'][self.animation_frame % len(animation['delays'])] self.animation_delay -= animation['delays'][self.animation_frame % len(animation['delays'])]
@ -40,11 +38,11 @@ class Sprite(UiElement):
if state in self.spritesheet.animations: if state in self.spritesheet.animations:
self.animation_state = state self.animation_state = state
self.animation_delay = 0 self.animation_delay = 0
self.tick(0) self.tick(TickData(0, [], [], [], PositionScale()))
def set_animation_frame(self, frame: int): def set_animation_frame(self, frame: int):
self.animation_frame = frame self.animation_frame = frame
self.tick(0) self.tick(TickData(0, [], [], [], PositionScale()))
def render_sprite_image(self) -> pygame.Surface: def render_sprite_image(self) -> pygame.Surface:
return self.image return self.image

View File

@ -1,10 +1,7 @@
import json import json
import sys
from sprite.Spritesheet import Spritesheet from sprite.Spritesheet import Spritesheet
sys.path.append('./sprite')
# This class is used to load named sprite sheets from the img folder. # This class is used to load named sprite sheets from the img folder.
class SpritesheetManager: class SpritesheetManager:

View File

@ -1,11 +1,6 @@
import pygame
import sys
from sprite.Sprite import Sprite from sprite.Sprite import Sprite
from sprite.Spritesheet import Spritesheet from sprite.Spritesheet import Spritesheet
sys.path.append('./sprite')
class StaticSprite(Sprite): class StaticSprite(Sprite):
def __init__(self, spritesheet: Spritesheet): def __init__(self, spritesheet: Spritesheet):

View File

@ -1,11 +1,32 @@
import pygame
from pygame.event import Event from pygame.event import Event
from sprite.PositionScale import PositionScale
from ui_elements import CoordinateTransform
class ClickEvent: class ClickEvent:
CLICK_LEFT = 1
CLICK_MIDDLE = 2
CLICK_RIGHT = 3
def __init__(self, world_position: tuple[float, float], screen_position: tuple[float, float], event: Event): def __init__(self, world_position: tuple[float, float], screen_position: tuple[float, float], event: Event):
self.world_position = world_position self.world_position = world_position
self.screen_position = screen_position self.screen_position = screen_position
self.event = event self.event = event
def __str__(self): def is_click_down(self, button: int) -> bool:
return f"ClickEvent(world_position={self.world_position}, screen_position={self.screen_position})" return self.event.type == pygame.MOUSEBUTTONDOWN and self.event.button == button
def is_click_up(self, button: int) -> bool:
return self.event.type == pygame.MOUSEBUTTONUP and self.event.button == button
@staticmethod
def create_events(event: list[Event], screen_transform: PositionScale) -> list['ClickEvent']:
return [
ClickEvent(CoordinateTransform.transform_screen_to_world(pygame.mouse.get_pos(), screen_transform),
pygame.mouse.get_pos(),
e)
for e in event
if e.type == pygame.MOUSEBUTTONDOWN or e.type == pygame.MOUSEBUTTONUP
]

View File

@ -0,0 +1,21 @@
from sprite.PositionScale import PositionScale
def transform_screen_to_world(convert_position: tuple, screen_transform: PositionScale):
screen_scale = screen_transform.scale
screen_position = screen_transform.position
object_position = (convert_position[0] / screen_scale[0] - screen_position[0],
convert_position[1] / screen_scale[1] - screen_position[1])
return object_position
def transform_world_to_screen(convert_position: tuple, screen_transform: PositionScale):
screen_scale = screen_transform.scale
screen_position = screen_transform.position
object_position = (int((convert_position[0] + screen_position[0]) * screen_scale[0]),
(int(convert_position[1] + screen_position[1]) * screen_scale[1]))
return object_position

View File

@ -0,0 +1,31 @@
import pygame
from pygame.event import Event
class KeyEvent:
KEY_LEFT: list[int] = [pygame.K_LEFT, pygame.K_a]
KEY_RIGHT: list[int] = [pygame.K_RIGHT, pygame.K_d]
KEY_UP: list[int] = [pygame.K_UP, pygame.K_w]
KEY_DOWN: list[int] = [pygame.K_DOWN, pygame.K_s]
KEY_SPACE: list[int] = [pygame.K_SPACE]
KEY_ESCAPE: list[int] = [pygame.K_ESCAPE]
KEY_ENTER: list[int] = [pygame.K_RETURN, pygame.K_KP_ENTER]
def __init__(self, event: Event):
self.event = event
def is_key_down(self, key: int) -> bool:
return self.event.type == pygame.KEYDOWN and self.event.key == key
def is_key_up(self, key: int) -> bool:
return self.event.type == pygame.KEYUP and self.event.key == key
def is_keymap_down(self, keys: list[int]) -> bool:
return self.event.type == pygame.KEYDOWN and self.event.key in keys
def is_keymap_up(self, keys: list[int]) -> bool:
return self.event.type == pygame.KEYUP and self.event.key in keys
@staticmethod
def create_events(event: list[Event]) -> list['KeyEvent']:
return [KeyEvent(e) for e in event if e.type == pygame.KEYDOWN or e.type == pygame.KEYUP]

View File

@ -2,13 +2,14 @@ import pygame
from pygame import Surface, Rect from pygame import Surface, Rect
from pygame.font import Font from pygame.font import Font
from physics.TickData import TickData
from sprite.BoundingBox import BoundingBox from sprite.BoundingBox import BoundingBox
from sprite.PositionScale import PositionScale from sprite.PositionScale import PositionScale
from ui_elements.UiElement import UiElement from ui_elements.UiElement import UiElement
class TextLabel(UiElement): class TextLabel(UiElement):
def tick(self, dt: float): def tick(self, tick_data: TickData):
pass pass
def __init__(self, text: str, x_position: float, y_position: float, font_size: int, alignment: str = "left"): def __init__(self, text: str, x_position: float, y_position: float, font_size: int, alignment: str = "left"):

View File

@ -6,6 +6,8 @@ from pygame import Surface
from sprite.BoundingBox import BoundingBox from sprite.BoundingBox import BoundingBox
from sprite.PositionScale import PositionScale from sprite.PositionScale import PositionScale
from physics.TickData import TickData
from ui_elements import CoordinateTransform
class UiElement: class UiElement:
@ -25,7 +27,7 @@ class UiElement:
listener(self, click_event) listener(self, click_event)
@abc.abstractmethod @abc.abstractmethod
def tick(self, dt: float): def tick(self, tick_data: TickData):
pass pass
@abc.abstractmethod @abc.abstractmethod
@ -43,7 +45,7 @@ class UiElement:
image = self.render_sprite_image() image = self.render_sprite_image()
if image is not None: if image is not None:
target_position = UiElement.transform_world_to_screen(self.position_scale.position, screen_transform) target_position = CoordinateTransform.transform_world_to_screen(self.position_scale.position, screen_transform)
screen_scale = screen_transform.scale screen_scale = screen_transform.scale
object_scale = self.position_scale.scale object_scale = self.position_scale.scale
@ -57,23 +59,3 @@ class UiElement:
@staticmethod @staticmethod
def get_scaled_image(image, resize): def get_scaled_image(image, resize):
return pygame.transform.scale(image, resize) return pygame.transform.scale(image, resize)
@staticmethod
def transform_screen_to_world(convert_position: tuple, screen_transform: PositionScale):
screen_scale = screen_transform.scale
screen_position = screen_transform.position
object_position = (convert_position[0] / screen_scale[0] - screen_position[0],
convert_position[1] / screen_scale[1] - screen_position[1])
return object_position
@staticmethod
def transform_world_to_screen(convert_position: tuple, screen_transform: PositionScale):
screen_scale = screen_transform.scale
screen_position = screen_transform.position
object_position = (int((convert_position[0] + screen_position[0]) * screen_scale[0]),
(int(convert_position[1] + screen_position[1]) * screen_scale[1]))
return object_position