This commit is contained in:
Timéo Cézard 2024-01-03 19:58:49 +01:00
parent bac035f00d
commit d8e64348cd
13 changed files with 552 additions and 69 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
assets/textures/dada.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
assets/textures/dodo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View file

@ -146,10 +146,17 @@ class World(Entity):
- `entity`: l'entité dans lequelle ajouter le composant. - `entity`: l'entité dans lequelle ajouter le composant.
- `component`: le composant à ajouter. - `component`: le composant à ajouter.
""" """
if isinstance(component, tuple):
for c in component: # type: ignore
if c is not None:
self.set_component(entity, c) # type: ignore
return
self.__components.setdefault(entity.identifier, {})[type(component)] = component self.__components.setdefault(entity.identifier, {})[type(component)] = component
self.__entities.setdefault(type(component), set()).add(entity.identifier) self.__entities.setdefault(type(component), set()).add(entity.identifier)
def get_component[T]( def get_component[
T
](
self, entity: "Entity", component_type: type[T], default: Optional[T] = None self, entity: "Entity", component_type: type[T], default: Optional[T] = None
) -> T: ) -> T:
""" """

View file

@ -3,9 +3,58 @@ Module d'exemple de l'utilisation du moteur de jeu.
""" """
from engine import Scene, start_game from engine import Scene, start_game
from engine.ecs import World from engine.ecs import Entity, World
from plugins import defaults from engine.math import Vec2
from plugins.render import Origin, Position, Scale, Texture from plugins import defaults, physics
from plugins import render
from plugins.inputs import Held
from plugins.render import (
Origin,
Position,
Scale,
SpriteBundle,
)
from plugins.timing import Delta
def lol(a: Entity, b: Entity):
if Bounce in b:
speed = a[physics.Velocity].length
a[physics.Velocity] = a[physics.Velocity].normalized
a[physics.Velocity].y = (a[Position].y - b[Position].y) * 0.005
a[physics.Velocity] = a[physics.Velocity].normalized * min(
(speed * 1.5), 1000.0
)
return True
class Bounce:
pass
def lol_simul(a: Entity, b: Entity):
lol(a, b)
return RightWall not in b
class Simulated:
pass
class Bar:
pass
class RightWall:
pass
class BallFollow(int):
pass
class Mine:
pass
def __initialize(world: World): def __initialize(world: World):
@ -13,16 +62,154 @@ def __initialize(world: World):
Initialise les ressources pour le moteur de jeu. Initialise les ressources pour le moteur de jeu.
""" """
world.new_entity().set( world.new_entity().set(
Texture("background.png", 0), SpriteBundle(
# Scale(1000, 1000), "background.png",
# Origin(0.5, 0.5), -1,
# Position(600, 600), scale=Vec2(render.WIDTH, render.HEIGHT),
),
) )
# world.new_entity().set(
# SpriteBundle(
# "dodo.png",
# 0,
# position=Vec2(800, 500),
# origin=Vec2(0.5, 0.5),
# scale=Vec2(400, 300),
# ),
# physics.Solid(),
# )
world.new_entity().set(
SpriteBundle(
"dodo.png",
0,
position=Vec2(100, 100),
origin=Vec2(0, 0),
scale=Vec2(render.WIDTH - 200, 10),
),
physics.Solid(),
)
world.new_entity().set(
SpriteBundle(
"dodo.png",
0,
position=Vec2(100, render.HEIGHT - 100),
origin=Vec2(0, 1),
scale=Vec2(render.WIDTH - 200, 10),
),
physics.Solid(),
)
world.new_entity().set(
SpriteBundle(
"dodo.png",
0,
position=Vec2(render.WIDTH - 100, 100),
origin=Vec2(1, 0),
scale=Vec2(10, render.HEIGHT - 200),
),
physics.Solid(),
RightWall(),
)
world.new_entity().set(
SpriteBundle(
"dodo.png",
0,
position=Vec2(100, 100),
origin=Vec2(0, 0),
scale=Vec2(10, render.HEIGHT - 200),
),
physics.Solid(),
)
world.new_entity().set(
SpriteBundle(
"dodo.png",
0,
position=Vec2(render.WIDTH - 130, render.HEIGHT / 2),
origin=Vec2(0.5, 0.5),
scale=Vec2(10, 200),
),
physics.Solid(),
Bar(),
Bounce(),
)
world.new_entity().set(
SpriteBundle(
"dodo.png",
0,
position=Vec2(130, render.HEIGHT / 2),
origin=Vec2(0.5, 0.5),
scale=Vec2(10, 200),
),
physics.Solid(),
Mine(),
Bounce(),
)
world.new_entity().set(
SpriteBundle(
"dada.png",
1,
scale=Vec2(10),
position=Vec2(500, 500),
origin=Vec2(0.5, 0.5),
),
physics.Velocity(Vec2(200, 100)),
physics.CollisionHandler(lol),
)
def __update(world: World):
"""
Test.
"""
for entity in world.query(Mine, Position):
if "z" in world[Held]:
entity[Position].y -= 300 * world[Delta]
if "s" in world[Held]:
entity[Position].y += 300 * world[Delta]
ball = max(
world.query(Position, physics.Velocity, physics.CollisionHandler),
key=lambda e: e[Position].x,
)
for bar in world.query(Bar):
bar.remove(physics.Solid)
entity = world.new_entity()
entity.set(
Position(ball[Position]),
Scale(ball[Scale]),
physics.Velocity(ball[physics.Velocity]),
Origin(ball[Origin]),
physics.CollisionHandler(lol_simul),
)
physics.move_entity(entity, entity[physics.Velocity] * 500)
target = entity[Position].y
for bar in world.query(Bar):
diff = target - bar[Position].y
bar[Position].y += (diff / abs(diff)) * 300 * world[Delta]
bar.set(physics.Solid())
entity.destroy()
# ball.set(Simulated())
# for entity in world.query(Bar):
# entity.remove(physics.Solid)
# last_position = Vec2(ball[Position])
# last_velocity = Vec2(ball[physics.Velocity])
# physics.move_entity(ball, ball[physics.Velocity] * 5000)
# ball[Position] = last_position
# ball[physics.Velocity] = last_velocity
# for entity in world.query(Bar):
# entity.set(physics.Solid())
# ball.remove(Simulated)
MENU = Scene( MENU = Scene(
[__initialize], [__initialize],
[], [__update],
[], [],
) )

45
src/plugins/assets.py Normal file
View file

@ -0,0 +1,45 @@
"""
Ce module contient des utilitaires pour le chargement des ressources du jeu.
"""
import pygame
def load_texture(name: str, cache: dict[str, pygame.Surface] = {}) -> pygame.Surface:
"""
Charge une texture et la renvoi.
"""
surface = cache.get(name)
if surface is None:
surface = pygame.image.load(f"assets/textures/{name}").convert_alpha()
cache[name] = surface
return surface
def load_sound(
name: str, cache: dict[str, pygame.mixer.Sound] = {}
) -> pygame.mixer.Sound:
"""
Charge un son et le renvoi.
"""
sound = cache.get(name)
if sound is None:
sound = pygame.mixer.Sound(f"assets/sounds/{name}")
cache[name] = sound
return sound
def load_text(
text: str,
size: int,
color: pygame.Color,
cache: dict[tuple[str, int, tuple[int, int, int]], pygame.Surface] = {},
) -> pygame.Surface:
"""
Charge un texte et le renvoi.
"""
surface = cache.get((text, size, (color.r, color.g, color.b)))
if surface is None:
surface = pygame.font.Font("assets/font.ttf", size).render(text, True, color)
cache[(text, size, (color.r, color.g, color.b))] = surface
return surface

View file

@ -2,13 +2,12 @@
Un plugin permettant de savoir si l'on a cliqué sur une entité. Un plugin permettant de savoir si l'on a cliqué sur une entité.
""" """
from tkinter import Scale
from typing import Callable from typing import Callable
from engine import GlobalPlugin from engine import GlobalPlugin
from engine.ecs import Entity, World from engine.ecs import Entity, World
from plugins.hover import Hovered from plugins.hover import Hovered
from plugins.inputs import Pressed from plugins.inputs import Pressed
from plugins.render import Position from plugins.render import Origin, Position, Scale
class Clicked: class Clicked:
@ -31,7 +30,7 @@ def __update_clicked(world: World):
Met à jour les composants `Clicked`. Met à jour les composants `Clicked`.
""" """
mouse_click = "button_1" in world[Pressed] mouse_click = "button_1" in world[Pressed]
sprite_entities = world.query(Position, Scale) sprite_entities = world.query(Position, Scale, Origin)
for entity in sprite_entities: for entity in sprite_entities:
if Hovered in entity and mouse_click: if Hovered in entity and mouse_click:
entity[Clicked] = Clicked() entity[Clicked] = Clicked()

View file

@ -2,14 +2,16 @@
Plugin qui rassemple tous les plugins globaux. Plugin qui rassemple tous les plugins globaux.
""" """
from plugins import display, inputs, sound, render, timing, hover from plugins import click, display, inputs, physics, sound, render, timing, hover
PLUGIN = ( PLUGIN = (
display.PLUGIN display.PLUGIN
+ timing.PLUGIN + timing.PLUGIN
+ inputs.PLUGIN + inputs.PLUGIN
+ physics.PLUGIN
+ hover.PLUGIN + hover.PLUGIN
+ click.PLUGIN
+ sound.PLUGIN + sound.PLUGIN
+ render.PLUGIN + render.PLUGIN
) )

View file

@ -44,10 +44,10 @@ def __update_hovered(world: World):
""" """
# On met à jour les composants # On met à jour les composants
mouse_position = world[MousePosition] mouse_position = world[MousePosition]
for entity in world.query(Position, Scale): for entity in world.query(Position, Scale, Origin):
# Récupération de la position et taille de l'entité # Récupération de la position et taille de l'entité
size = entity[Scale] size = entity[Scale]
position = entity[Position] - (entity.get(Origin, Origin(0)) * size) position = entity[Position] - (entity[Origin] * size)
# On détermine si la souris est sur l'entité # On détermine si la souris est sur l'entité
if ( if (

194
src/plugins/physics.py Normal file
View file

@ -0,0 +1,194 @@
"""
Plugin implémentant une physique exacte pour des collisions AABB.
"""
from typing import Callable
from engine import GlobalPlugin
from engine.ecs import Entity, World
from engine.math import Vec2
from plugins.render import Origin, Position, Scale
from plugins.timing import Delta
class Solid:
"""
Composant représentant un objet (de préférence imobille pour que la simulation soit prédictible) qui ne laisse pas passer les objets dynamiques.
"""
class Velocity(Vec2):
"""
Composant donnant la vélocité d'un objet.
"""
class CollisionHandler:
"""
Composant permettant de traiter les collisions.
"""
def __init__(self, callback: Callable[[Entity, Entity], bool]):
self.callback = callback
class AABB:
"""
Définit une boite.
"""
def __init__(self, min: Vec2, max: Vec2, entity: Entity):
self.min = min
self.max = max
self.entity = entity
@staticmethod
def from_entity(entity: Entity):
min = entity[Position] - entity[Origin] * entity[Scale]
return AABB(min, min + entity[Scale], entity)
def entity_position(self, entity: Entity):
scale = self.max - self.min
entity[Position] = self.min + entity[Origin] * scale
entity[Scale] = scale
def __contains__(self, point: Vec2):
return (
self.min.x <= point.x <= self.max.x and self.min.y <= point.y <= self.max.y
)
def move(self, movement: Vec2):
self.min += movement
self.max += movement
def line_to_line(sa: Vec2, ea: Vec2, sb: Vec2, eb: Vec2):
"""
Renvoie la collision entre deux lignes.
"""
if sa.x == ea.x:
sa.x += 0.0001
if sb.x == eb.x:
sb.x += 0.0001
if sa.y == ea.y:
sa.y += 0.0001
if sb.y == eb.y:
sb.y += 0.0001
divisor = (eb.y - sb.y) * (ea.x - sa.x) - (eb.x - sb.x) * (ea.y - sa.y)
if divisor == 0:
uA = 0
else:
uA = ((eb.x - sb.x) * (sa.y - sb.y) - (eb.y - sb.y) * (sa.x - sb.x)) / (divisor)
divisor = (eb.y - sb.y) * (ea.x - sa.x) - (eb.x - sb.x) * (ea.y - sa.y)
if divisor == 0:
uB = 0
else:
uB = ((ea.x - sa.x) * (sa.y - sb.y) - (ea.y - sa.y) * (sa.x - sb.x)) / (divisor)
if uA >= 0 and uA <= 1 and uB >= 0 and uB <= 1:
return (
Vec2((uA * (ea.x - sa.x)), (uA * (ea.y - sa.y))).length / (ea - sa).length
)
return 1.0
def line_to_aabb(start: Vec2, end: Vec2, aabb: AABB):
"""
Renvoie la collision entre une ligne et une AABB.
"""
left = line_to_line(start, end, aabb.min, Vec2(aabb.min.x, aabb.max.y))
right = line_to_line(start, end, Vec2(aabb.max.x, aabb.min.y), aabb.max)
bottom = line_to_line(start, end, aabb.min, Vec2(aabb.max.x, aabb.min.y))
top = line_to_line(start, end, Vec2(aabb.min.x, aabb.max.y), aabb.max)
t = min([left, right, bottom, top])
if t == left:
normal = Vec2(-1, 0)
elif t == right:
normal = Vec2(1, 0)
elif t == bottom:
normal = Vec2(0, -1)
elif t == top:
normal = Vec2(0, 1)
else:
normal = Vec2(0, 0)
return t, normal
def aabb_to_aabb(moving: AABB, static: AABB, movement: Vec2):
"""
Renvoie la collision entre deux AABB.
"""
size = (moving.max - moving.min) / 2
static = AABB(static.min - size, static.max + size, static.entity)
start_pos = moving.min + size
return line_to_aabb(start_pos, start_pos + movement, static)
def aabb_to_aabbs(moving: AABB, statics: list[AABB], movement: Vec2):
"""
Renvoie la collision entre deux AABB.
"""
t = 1.0
normal = Vec2(0, 0)
entity = None
for static in statics:
if static.entity == moving.entity:
continue
result = aabb_to_aabb(moving, static, movement)
if result[0] < t:
t = result[0]
normal = result[1]
entity = static.entity
return t, normal, entity
def move_entity(entity: Entity, movement: Vec2, disable_callback: bool = False):
world = entity.world
aabb = AABB.from_entity(entity)
others = [
AABB.from_entity(other) for other in world.query(Solid, Position, Scale, Origin)
]
counter = 0
while movement.length > 0.0001 and counter < 50:
t, normal, obstacle = aabb_to_aabbs(aabb, others, movement)
if t == 1.0:
step = movement
else:
step = movement * max(t - 0.000001, 0)
aabb.move(step)
aabb.entity_position(entity)
movement -= step
if normal.x != 0:
movement.x *= -1
entity[Velocity].x *= -1
if normal.y != 0:
movement.y *= -1
entity[Velocity].y *= -1
movement /= entity[Velocity]
if obstacle is not None and not disable_callback:
if not entity.get(
CollisionHandler, CollisionHandler(lambda e, o: True)
).callback(entity, obstacle):
break
if not obstacle.get(
CollisionHandler, CollisionHandler(lambda e, o: True)
).callback(obstacle, entity):
break
movement *= entity[Velocity]
counter += 1
def __apply_velocity(world: World):
"""
Applique la vélocité a toutes les entitées.
"""
delta = world[Delta]
for entity in world.query(Velocity, Position, Scale, Origin):
move_entity(entity, entity[Velocity] * delta)
PLUGIN = GlobalPlugin(
[],
[__apply_velocity],
[],
[],
)

View file

@ -3,9 +3,10 @@ Un plugin qui s'occupe de rendre des choses dans la fenetre.
""" """
import pygame import pygame
from engine import GlobalPlugin, KeepAlive from engine import GlobalPlugin
from engine.ecs import World from engine.ecs import World
from engine.math import Vec2 from engine.math import Vec2
from plugins import assets
WIDTH = 1440 WIDTH = 1440
@ -28,6 +29,52 @@ def calculate_surface_rect() -> tuple[float, float, float, float]:
return offset, 0.0, target_width, float(height) return offset, 0.0, target_width, float(height)
class SpriteBundle:
"""
Un assemblage de composants permettant de faire une sprite.
"""
def __new__(
cls,
texture: str,
order: float,
position: Vec2 = Vec2(0),
scale: Vec2 = Vec2(128),
origin: Vec2 = Vec2(0),
):
return (
Texture(texture),
Order(order),
Position(position),
Scale(scale),
Origin(origin),
)
class TextBundle:
"""
Un assemblage de composants permettant de faire un texte.
"""
def __new__(
cls,
text: str,
order: float,
size: int = 50,
color: pygame.Color = pygame.Color(255, 255, 255),
position: Vec2 = Vec2(0),
origin: Vec2 = Vec2(0),
):
return (
Text(text),
TextSize(size),
TextColor(color),
Position(position),
Order(order),
Origin(origin),
)
class Texture(str): class Texture(str):
""" """
Composant donnant le nom de la texture d'une entité. Composant donnant le nom de la texture d'une entité.
@ -58,37 +105,44 @@ class Origin(Vec2):
""" """
class Surface(KeepAlive, pygame.Surface): class Text(str):
""" """
Ressource qui stocke la surface de rendu du jeu. Composant donnant le texte d'une entité.
""" """
def __initialize(world: World): class TextSize(int):
""" """
Prépare le monde pour la gestion du rendu. Composant donnant la taille du texte d'une entité.
""" """
world.set(Surface((WIDTH, HEIGHT)))
def __render(world: World, cache: dict[str, pygame.Surface] = {}): class TextColor(pygame.Color):
"""
Composant donnant la couleur du texte d'une entité.
"""
def __render(world: World, surface: pygame.Surface = pygame.Surface((WIDTH, HEIGHT))):
""" """
Rend le monde du jeu sur la surface puis l'affiche sur la fenetre. Rend le monde du jeu sur la surface puis l'affiche sur la fenetre.
""" """
# On rend le monde sur la surface # On rend le monde sur la surface
surface: Surface = world[Surface] entities = world.query(Texture, Position, Order, Scale, Origin)
entities = sorted(world.query(Texture), key=lambda entity: entity.get(Order, -1)) entities.update(world.query(Text, Position, Order, Origin, TextSize, TextColor))
entities = sorted(entities, key=lambda entity: entity[Order])
for entity in entities: for entity in entities:
texture_name = entity[Texture] if Text in entity:
texture = cache.get(texture_name) texture = assets.load_text(
if texture is None: entity[Text], entity[TextSize], entity[TextColor]
texture = pygame.image.load(f"assets/textures/{texture_name}") )
cache[texture_name] = texture scale = Scale(texture.get_width(), texture.get_height())
scale = entity.get(Scale, Scale(128)) else:
texture = pygame.transform.scale(texture, (scale.x, scale.y)) texture = entity[Texture]
position = ( texture = assets.load_texture(texture)
entity.get(Position, Position(0)) - (entity.get(Origin, Origin(0))) * scale scale = entity[Scale]
) texture = pygame.transform.scale(texture, (scale.x, scale.y))
position = entity[Position] - entity[Origin] * scale
surface.blit(texture, (position.x, position.y)) surface.blit(texture, (position.x, position.y))
# On affiche la surface sur la fenetre # On affiche la surface sur la fenetre
@ -103,7 +157,7 @@ def __render(world: World, cache: dict[str, pygame.Surface] = {}):
PLUGIN = GlobalPlugin( PLUGIN = GlobalPlugin(
[__initialize], [],
[], [],
[__render], [__render],
[], [],

View file

@ -5,43 +5,38 @@ Un plugin permettant de jouer des sons.
from typing import Callable from typing import Callable
import pygame import pygame
from engine import GlobalPlugin, KeepAlive from engine import GlobalPlugin
from engine.ecs import Entity, World from engine.ecs import Entity, World
from plugins import assets
class Channels(KeepAlive, dict[Entity, pygame.mixer.Channel]): class Sound(str):
"""
Ressource qui stoque les sons actuellement joués dans le jeu.
"""
class Sound:
""" """
Composant permettant de jouer un son. Composant permettant de jouer un son.
""" """
def __init__(
self, class Volume(float):
sound: str, """
loop: bool = False, Composant donnant le volume d'un son.
volume: float = 1.0, """
fade_ms: int = 0,
callback: Callable[[World, Entity], object] = lambda _w, _e: None,
): class Loop:
self.sound = sound """
self.loop = loop Composant indiquant si le son joué par l'entité doit se relancer en boucle.
self.volume = volume """
self.fade_ms = fade_ms
class SoundCallback:
"""
Composant donnant une fonction qui sera appelée à la fin du son.
"""
def __init__(self, callback: Callable[[World, Entity], object]):
self.callback = callback self.callback = callback
def __initialize(world: World):
"""
Ajoute les ressources utiles pour le plugin.
"""
world.set(Channels())
def __update_sounds( def __update_sounds(
world: World, world: World,
channels: dict[Entity, pygame.mixer.Channel] = {}, channels: dict[Entity, pygame.mixer.Channel] = {},
@ -51,23 +46,23 @@ def __update_sounds(
Met à jour les sons du jeu. Met à jour les sons du jeu.
""" """
# Ajout des sons non gérés # Ajout des sons non gérés
channels = world[Channels]
sound_entities = world.query(Sound) sound_entities = world.query(Sound)
for entity in sound_entities: for entity in sound_entities:
if entity not in channels: if entity not in channels:
sound = entity[Sound] sound_name = entity[Sound]
channel = sound.sound.play(sound.loop, fade_ms=sound.fade_ms) sound = assets.load_sound(sound_name)
channel = sound.play(Loop in entity)
if channel is not None: # type: ignore if channel is not None: # type: ignore
channel.set_volume(sound.volume) channel.set_volume(entity.get(Volume, 1.0))
channels[entity] = channel channels[entity] = channel
# On supprime les sons qui sont arrêtés ou qui n'ont plus d'entité # On supprime les sons qui sont arrêtés ou qui n'ont plus d'entité
channels_to_remove: list[Entity] = [] channels_to_remove: list[Entity] = []
for entity, channel in channels.items(): for entity, channel in channels.items():
if not channel.get_busy() and Sound in entity: if not channel.get_busy() and Sound in entity:
callback = entity[Sound].callback callback = entity.get(SoundCallback, SoundCallback(lambda w, e: None))
del entity[Sound] del entity[Sound]
callback(world, entity) callback.callback(world, entity)
channels_to_remove.append(entity) channels_to_remove.append(entity)
elif entity not in sound_entities: elif entity not in sound_entities:
channel.stop() channel.stop()
@ -77,7 +72,7 @@ def __update_sounds(
PLUGIN = GlobalPlugin( PLUGIN = GlobalPlugin(
[__initialize], [],
[], [],
[__update_sounds], [__update_sounds],
[], [],