Merge remote-tracking branch 'origin/main'

main
Stephan Halder 2023-03-26 17:20:04 +02:00
commit 34f3203c7a
18 changed files with 218 additions and 110 deletions

View File

@ -12,21 +12,21 @@
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,x,x,x,x,x,x,x,x,x,,,,,,,,,,,,,,,,,D,,,
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#
#,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,x,x,x,x,x,x,x,x,x,,,,,,,,,,,,,,,,,D,,,#
#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,,,,,,,,,,,,,,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#
#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,,,,,,,,,,,,,,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#
#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,,,,,,,,,,,,,,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#
@ -38,6 +38,4 @@
#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,,,,,,,,,,,,,,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#
#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,,,,,,,,,,,,,,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#
#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,,,,,,,,,,,,,,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#
#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,,,,,,,,,,,,,,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
3,3,id=HEBEL,requires=HEBEL;HEBEL-2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,,,,,,,,,,,,,,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#,#
1
12
13
14
15 #
16 #
17 #
18 #
19 #
20 #
21 # #
22 # #
23 # #
24 # #
25 # #
26 # #
27 # #
28 # #
29 # x x x x x x x x x D #
30 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
31 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
32 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
38 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
39 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
40 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
41 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
3 3 id=HEBEL requires=HEBEL;HEBEL-2

View File

@ -1,7 +1,7 @@
[
{
"name": "1-1",
"theme": "tutorial",
"theme": "castle",
"abilities": [
"dash"
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

View File

@ -117,5 +117,34 @@
"height": 16
}
]
},
{
"id": "castle_block_full",
"subsheets": [
{
"id": "1",
"delays": [
1
],
"width": 12,
"height": 12
},
{
"id": "2",
"delays": [
1
],
"width": 12,
"height": 12
},
{
"id": "3",
"delays": [
1
],
"width": 12,
"height": 12
}
]
}
]

View File

@ -1,3 +1,5 @@
import random
from level.Level import Level
from level.LevelElementSymbols import LevelElementSymbols
from level.elements.BlockElement import BlockElement
@ -30,7 +32,7 @@ class LoadedLevel:
if tile_element in LevelElementSymbols.BLOCKS_LIST:
spritesheet = self.spritesheet_manager.get_sheet(level.theme + tile_element['sprite_id'])
sprite = BlockElement(spritesheet)
sprite.set_animation_state('1')
sprite.set_animation_state(str(random.randint(0, 3)))
sprite.position_scale.position = position
elif tile_element == LevelElementSymbols.GOAL_DOOR:

View File

@ -6,7 +6,7 @@ from level.LevelManager import LevelManager
from level.elements.LoadedLevel import LoadedLevel
from physics.SpriteManager import SpriteManager, DrawLayers
from physics.TickData import TickData
from physics.controllers.PlayerSprite import PlayerSprite
from physics.sprites.PlayerSprite import PlayerSprite
from sprite.PositionScale import PositionScale
from sprite.SpritesheetManager import SpritesheetManager
from sprite.Sprite import Sprite
@ -47,6 +47,16 @@ if what_to_run == 'level':
generated_level = LoadedLevel(sprite_manager, spritesheet_manager)
generated_level.load_level(parsed_levels_manager.levels[0])
ghost_character = PlayerSprite(spritesheet_manager.get_sheet("ghost_character"))
ghost_character.position_scale = PositionScale((90, 50), (1, 1))
sprite_manager.add_ui_element(DrawLayers.OBJECTS, ghost_character)
text_1 = TextLabel("Frame: 0", 2, 110, 50, alignment="left")
text_1.position_scale.scale = (0.3, 0.3)
sprite_manager.add_ui_element(DrawLayers.UI, text_1)
ghost_character.debug_label = text_1
while True:
clock.tick(frame_rate)

View File

@ -0,0 +1,14 @@
class CollisionDirection:
LEFT = 0
RIGHT = 1
TOP = 2
BOTTOM = 3
DIRECTION_NAMES = ['LEFT', 'RIGHT', 'TOP', 'BOTTOM']
def __init__(self, direction: int, primary_sprite, secondary_sprite):
self.direction = direction
self.primary_sprite = primary_sprite
self.secondary_sprite = secondary_sprite
def to_string(self):
return 'CollisionDirection.' + CollisionDirection.DIRECTION_NAMES[self.direction]

View File

@ -1,5 +1,6 @@
from typing import Optional
from physics.CollisionDirection import CollisionDirection
from physics.TickData import TickData
from sprite.DynamicSprite import DynamicSprite
from sprite.Sprite import Sprite
@ -40,6 +41,8 @@ class PhysicsElementsHandler:
# 4.2.3. If it doesn't, move the sprite to the new position
colliders = [sprite for sprite in sprites if isinstance(sprite, StaticSprite) and sprite.is_collider]
for collider in colliders:
collider.reset_collides_with()
dynamic_sprites = [sprite for sprite in sprites if isinstance(sprite, DynamicSprite)]
sorted_dynamic_sprites = sorted(dynamic_sprites, key=lambda spr: spr.position_scale.position[1])
@ -55,56 +58,65 @@ class PhysicsElementsHandler:
total_motion = sprite.motion
motion_step = ((total_motion[0] * dt) / motion_steps, (total_motion[1] * dt) / motion_steps)
collides_with_last = None
collided = [False, False]
sprite.reset_collides_with()
for i in range(motion_steps):
sprite.reset_touches()
sprite.position_scale.position = (
sprite.position_scale.position[0] + motion_step[0],
sprite.position_scale.position[1]
)
collides_with = self.check_collides(sprite, colliders)
if collides_with is not None:
if not collided[0]:
sprite.position_scale.position = (
sprite.position_scale.position[0] - motion_step[0],
sprite.position_scale.position[0] + motion_step[0],
sprite.position_scale.position[1]
)
if sprite.motion[0] > 0:
sprite.set_touches_right(True)
collides_with = self.check_collides(sprite, colliders)
if collides_with is not None:
sprite.position_scale.position = (
sprite.position_scale.position[0] - motion_step[0],
sprite.position_scale.position[1]
)
if sprite.motion[0] < 0:
sprite.set_touches_left(True)
if sprite.motion[0] > 0:
sprite.add_collides_with(CollisionDirection(CollisionDirection.RIGHT, sprite, collides_with))
sprite.motion = (0, sprite.motion[1])
return collides_with
if sprite.motion[0] < 0:
sprite.add_collides_with(CollisionDirection(CollisionDirection.LEFT, sprite, collides_with))
sprite.position_scale.position = (
sprite.position_scale.position[0],
sprite.position_scale.position[1] + motion_step[1]
)
sprite.motion = (0, sprite.motion[1])
collides_with = self.check_collides(sprite, colliders)
if collides_with is not None:
collides_with_last = collides_with
collided[0] = True
if not collided[1]:
sprite.position_scale.position = (
sprite.position_scale.position[0],
sprite.position_scale.position[1] - motion_step[1]
sprite.position_scale.position[1] + motion_step[1]
)
if sprite.motion[1] > 0:
sprite.set_touches_bottom(True)
collides_with = self.check_collides(sprite, colliders)
if collides_with is not None:
sprite.position_scale.position = (
sprite.position_scale.position[0],
sprite.position_scale.position[1] - motion_step[1]
)
if sprite.motion[1] < 0:
sprite.set_touches_top(True)
if sprite.motion[1] > 0:
sprite.add_collides_with(CollisionDirection(CollisionDirection.BOTTOM, sprite, collides_with))
sprite.motion = (sprite.motion[0], 0)
return collides_with
if sprite.motion[1] < 0:
sprite.add_collides_with(CollisionDirection(CollisionDirection.TOP, sprite, collides_with))
return None
sprite.motion = (sprite.motion[0], 0)
collides_with_last = collides_with
collided[1] = True
return collides_with_last
def check_collides(self, sprite: StaticSprite, colliders: list[StaticSprite]) -> Optional[StaticSprite]:
for collider in colliders:
if sprite is not collider and sprite.collides_with(collider, TOLERANCE):
return collider
if sprite is not collider:
if sprite.collides_with(collider, TOLERANCE):
return collider
return None

View File

@ -1,6 +1,5 @@
from typing import Optional
import pygame
from pygame import Surface
from physics.PhysicsElementsHandler import PhysicsElementsHandler
@ -57,5 +56,11 @@ class SpriteManager:
return elements
def collision_detected(self, sprite_a: Sprite, sprite_b: Sprite):
# print(f"Collision detected between {sprite_a} and {sprite_b}")
pass
def find_sprite_by_uuid(self, uuid: str) -> Optional[UiElement]:
for layer in DrawLayers.DRAW_ORDER:
for sprite in self.layers[layer]:
if sprite.uuid == uuid:
return sprite
return None

View File

@ -1,24 +0,0 @@
from physics.TickData import TickData
from sprite.DynamicSprite import DynamicSprite
from sprite.Spritesheet import Spritesheet
from ui_elements.KeyManager import KeyManager
from ui_elements.TextLabel import TextLabel
class PlayerSprite(DynamicSprite):
def __init__(self, spritesheet: Spritesheet):
super().__init__(spritesheet)
self.jump_time = -1
self.allowed_jump_time = 20
self.debug_label = TextLabel('', -1, -1)
def tick(self, tick_data: TickData):
super().tick(tick_data)
if tick_data.key_manager.is_keymap_down(KeyManager.KEY_RIGHT):
self.motion = (self.motion[0] + 2, self.motion[1])
if tick_data.key_manager.is_keymap_down(KeyManager.KEY_LEFT):
self.motion = (self.motion[0] - 2, self.motion[1])
self.debug_label.set_text(f'jump: {self.jump_time}, x: {round(self.motion[0], 2)}, y: {round(self.motion[1], 2)}, touches: {self.touches_bounding}')

View File

@ -0,0 +1,11 @@
from physics.TickData import TickData
from sprite.Spritesheet import Spritesheet
from sprite.StaticSprite import StaticSprite
class DeathBox(StaticSprite):
def __init__(self, spritesheet: Spritesheet):
super().__init__(spritesheet)
def tick(self, tick_data: TickData):
super().tick(tick_data)

View File

@ -0,0 +1,45 @@
from physics.CollisionDirection import CollisionDirection
from physics.TickData import TickData
from sprite.DynamicSprite import DynamicSprite
from sprite.Spritesheet import Spritesheet
from ui_elements.KeyManager import KeyManager
from ui_elements.TextLabel import TextLabel
class PlayerSprite(DynamicSprite):
def __init__(self, spritesheet: Spritesheet):
super().__init__(spritesheet)
self.debug_label = TextLabel('', -1, -1)
self.jump_time = -1
self.allowed_jump_time = 12
self.acceleration_horizontal = 2
self.deceleration_horizontal_air = 0.02
self.deceleration_horizontal_ground = 0.3
self.gravity = 9.81 / 10
self.max_motion_horizontal_via_input = 5
def tick(self, tick_data: TickData):
super().tick(tick_data)
if tick_data.key_manager.is_keymap_down(KeyManager.KEY_RIGHT):
if self.motion[0] < self.max_motion_horizontal_via_input:
self.motion = (self.motion[0] + self.acceleration_horizontal, self.motion[1])
if tick_data.key_manager.is_keymap_down(KeyManager.KEY_LEFT):
if self.motion[0] > -self.max_motion_horizontal_via_input:
self.motion = (self.motion[0] - self.acceleration_horizontal, self.motion[1])
if tick_data.key_manager.is_keymap_down(KeyManager.KEY_UP):
if self.jump_time < 0 and self.get_collides_with_direction(CollisionDirection.BOTTOM):
self.jump_time = self.allowed_jump_time
self.motion = (self.motion[0], self.motion[1] - 7)
if self.jump_time >= 0:
self.motion = (self.motion[0], self.motion[1] - 0.5)
if self.jump_time >= 0:
self.jump_time -= 1
self.debug_label.set_text(f'jump: {self.jump_time}, x: {round(self.motion[0], 2)}, y: {round(self.motion[1], 2)}, touches: {list(map(lambda x: x.to_string(), self.get_collides_with()))}')

View File

@ -1,4 +1,4 @@
from sprite.BoundingBox import BoundingBox
from physics.CollisionDirection import CollisionDirection
from sprite.Spritesheet import Spritesheet
from sprite.StaticSprite import StaticSprite
from physics.TickData import TickData
@ -10,39 +10,23 @@ class DynamicSprite(StaticSprite):
self.motion = (0, 0)
self.apply_base_deceleration = True
self.deceleration_horizontal_air = 0.02
self.deceleration_horizontal_ground = 0.3
self.gravity = 9.81 / 10
# up, right, down, left
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
if self.apply_base_deceleration:
deceleration_horizontal = 0
if abs(self.motion[0]) > 0:
if self.get_collides_with_direction(CollisionDirection.BOTTOM):
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):
self.touches_bounding = (self.touches_bounding[0], self.touches_bounding[1], value, self.touches_bounding[3])
def set_touches_right(self, value: bool):
self.touches_bounding = (self.touches_bounding[0], value, self.touches_bounding[2], self.touches_bounding[3])
def set_touches_left(self, value: bool):
self.touches_bounding = (self.touches_bounding[0], self.touches_bounding[1], self.touches_bounding[2], value)
def set_touches_top(self, value: bool):
self.touches_bounding = (value, self.touches_bounding[1], self.touches_bounding[2], self.touches_bounding[3])
def reset_touches(self):
self.touches_bounding = (False, False, False, False)
self.motion = (
self.motion[0] - deceleration_horizontal * self.motion[0] * tick_data.dt,
self.motion[1] + self.gravity * tick_data.dt
)

View File

@ -1,7 +1,7 @@
class PositionScale:
def __init__(self, position: tuple[float, float] = (0, 0), scale: tuple[float, float] = (1, 1)):
self.position = position
self.scale = scale
self.position: tuple[float, float] = position
self.scale: tuple[float, float] = scale
def apply_scale_to_position(self):
return self.position[0] * self.scale[0], self.position[1] * self.scale[1]

View File

@ -1,5 +1,8 @@
from typing import Optional
import pygame
from physics.CollisionDirection import CollisionDirection
from physics.TickData import TickData
from sprite.BoundingBox import BoundingBox
from sprite.PositionScale import PositionScale
@ -22,6 +25,22 @@ class Sprite(UiElement):
self.image = None
self.is_collider = True
self.collides_with_elements: list[CollisionDirection] = []
def add_collides_with(self, collision_direction: CollisionDirection):
self.collides_with_elements.append(collision_direction)
def reset_collides_with(self):
self.collides_with_elements = []
def get_collides_with(self) -> list[CollisionDirection]:
return self.collides_with_elements
def get_collides_with_direction(self, direction: int) -> Optional[CollisionDirection]:
for collision_direction in self.collides_with_elements:
if collision_direction.direction == direction:
return collision_direction
return None
def tick(self, tick_data: TickData):
animation = self.spritesheet.animations[self.animation_state]

View File

@ -1,4 +1,5 @@
import abc
import uuid
from typing import Optional
import pygame
@ -16,6 +17,8 @@ class UiElement:
self.visible = True
self.click_listeners = []
self.uuid = uuid.uuid4()
def add_click_listener(self, listener):
self.click_listeners.append(listener)