- Game Engine implemented on Database Engine (in memory with pandas)
- world is a database
- archetype is a table
- entity is a row(id)
- componenten is one column
- system is a function
- query is a database query
- Now with pydantic https://docs.pydantic.dev/latest/
I have long wanted to program games in Pygame using the Entity Component System (ECS) game design pattern. Once when I looked at the Bevy game framework in Rust, I was hooked. I wanted something like that in Python too. Since I couldn't find such a framework anywhere, I started developing a Python ECS framework myself - wonderworld
The data-oriented programming (DOP) approach tries to strictly separate data and program from each other.
One way to do this is with the Entity Component System (ECS) design pattern.
A component in the ECS design pattern contains only data.
Examples:
- Transform(x,y) - TransformComponent
- Speed (x,y) - SpeedComponent
- Sprite(image) - ImageComponent
A component does not contain any processing logic.
An entity is a game object, such as the knight. It consists of any number of components and does not itself contain any data or program logic. A unique ID linked to the components via an internal data structure is sufficient for the implementation of an entity.
A system contains the complete processing logic, but no own data. A game can consist of any number of systems. A system reads components and transforms their current state into another.
For example, the Movement system retrieves all speed components (SpeedComponents) and transform components (PositionComponent) in order to calculate the new position from them.
from wonderworld import *
PADDLE_VELOCITY: float = 180.0
class Paddle(Component):
pass
def setup():
commands = get_commands()
assets = get_resource(Assets)
# asset_server = get_resource(AssetServer)
# image_handle = asset_server.load("box.png");
image_handle = assets.add(create_image(color=pygame.Color('white'),
size=Vector2(10.0, 100.0)))
# camera
commands.spawn(Camera2dBundle(camera=Camera(),
transform=Transform(translation=Vector2(0,0),
rotation=0,
scale=Vector2(0, 0))))
# paddle
commands.spawn(SpriteBundle(sprite=Sprite(image=image_handle),
transform=Transform(translation=Vector2(100, 100),
rotation=0,
scale=Vector2(0, 0)),
visibility=Visibility(visible=True))) \
.insert(Paddle())
def input():
time = get_resource(Time)
keyboard_input = get_resource(Keyboard)
for transform, in Query(Transform, With(Paddle)):
paddle_move = time.delta_seconds * PADDLE_VELOCITY
if keyboard_input.pressed(K_UP):
transform.translation.y = transform.translation.y - paddle_move
if keyboard_input.pressed(K_DOWN):
transform.translation.y = transform.translation.y + paddle_move
if __name__ == "__main__":
App().insert_resource(ClearColor(color=pygame.Color('black'))) \
.add_plugins(DefaultPlugins()) \
.add_startup_system(setup) \
.add_system(input) \
.run()
pygame 2.1.2 (SDL 2.0.18, Python 3.9.16)
Hello from the pygame community. https://www.pygame.org/contribute.html
from wonderworld import *
HEIGHT = 600
WIDTH = 800
PADDLE_WIDTH: float = 10.0
PADDLE_HEIGHT: float = 100.0
PADDLE_VELOCITY: float = 180.0
PADDLE_MAX_MOVE: float = HEIGHT - PADDLE_HEIGHT
PADDLE_MIN_MOVE: float = 0
BALL_WIDTH: float = 10.0
BALL_VELOCITY: Vector2 = Vector2(160.0, 160.0)
LINE_WIDTH: float = 2.0;
class Score(Resource):
left: int
right: int
class Ball(Component):
pass
class Paddle(Component):
up_key: int
down_key: int
class Velocity(Component):
value: Vector2
class LeftText(Component):
pass
class RightText(Component):
pass
def setup():
commands = get_commands()
assets = get_resource(Assets)
# camera
commands.spawn(Camera2dBundle(camera=Camera(),
transform=Transform()))
# ball
ball_handle = assets.add(create_image(color=pygame.Color('white'),
size=Vector2(BALL_WIDTH, BALL_WIDTH)))
commands.spawn(SpriteBundle(sprite=Sprite(image=ball_handle),
transform=Transform(translation=Vector2(100, 100)),
visibility=Visibility(visible=True))) \
.insert(Velocity(value=BALL_VELOCITY.copy())) \
.insert(Ball())
# line
line_handle = assets.add(create_image(color=pygame.Color('white'),
size=Vector2(LINE_WIDTH, HEIGHT)))
commands.spawn(SpriteBundle(sprite=Sprite(image=line_handle),
transform=Transform(translation=Vector2(WIDTH / 2, 0)),
visibility=Visibility(visible=True)))
# paddle_left
left_handle = assets.add(create_image(color=pygame.Color('white'),
size=Vector2(PADDLE_WIDTH, PADDLE_HEIGHT)))
commands.spawn(SpriteBundle(sprite=Sprite(image=left_handle),
transform=Transform(translation=Vector2(0.0,
HEIGHT / 2.0 - PADDLE_HEIGHT / 2)),
visibility=Visibility(visible=True))) \
.insert(Paddle(up_key=K_w, down_key=K_a))
# paddle_right
right_handle = assets.add(create_image(color=pygame.Color('white'),
size=Vector2(PADDLE_WIDTH, PADDLE_HEIGHT)))
commands.spawn(SpriteBundle(sprite=Sprite(image=right_handle),
transform=Transform(translation=Vector2(WIDTH - PADDLE_WIDTH,
HEIGHT / 2.0 - PADDLE_HEIGHT / 2)),
visibility=Visibility(visible=True))) \
.insert(Paddle(up_key=K_UP, down_key=K_DOWN))
# score
asset_server = get_resource(AssetServer)
font_size = 64
commands.spawn(Text2dBundle(
text = Text(text="0",
font = asset_server.load("fonts/FiraSans-Bold.ttf",AssetType.FONT,size=font_size),
font_size = font_size,
color = Color('white')
),
transform = Transform(translation=Vector2(WIDTH/4.0,200.)
))).insert(LeftText())
commands.spawn(Text2dBundle(
text=Text(text="0",
font=asset_server.load("fonts/FiraSans-Bold.ttf", AssetType.FONT, size=font_size),
font_size=font_size,
color=Color('white')
),
transform=Transform(translation=Vector2(3 *WIDTH / 4.0, 200.)
))).insert(RightText())
def input():
time = get_resource(Time)
keyboard = get_resource(Keyboard)
for (paddle, transform) in Query((Paddle, Transform)):
paddle_move = time.delta_seconds * PADDLE_VELOCITY
if keyboard.pressed(paddle.up_key):
transform.translation.y = max(PADDLE_MIN_MOVE, transform.translation.y - paddle_move)
if keyboard.pressed(paddle.down_key):
transform.translation.y = min(PADDLE_MAX_MOVE, transform.translation.y + paddle_move)
def move_ball():
time = get_resource(Time)
for (transform, velocity) in Query((Transform, Velocity), With(Ball)):
transform.translation += velocity.value * time.delta_seconds
if transform.translation.y <= 0 or transform.translation.y + BALL_WIDTH >= HEIGHT:
velocity.value.y = - velocity.value.y
def collides():
for ball_transform, velocity in Query((Transform, Velocity), With(Ball)):
ball_size = Vector2(BALL_WIDTH, BALL_WIDTH)
for paddle_transform, in Query(Transform, With(Paddle)):
paddle_size = Vector2(PADDLE_WIDTH, PADDLE_HEIGHT)
if collide_aabb(ball_transform.translation, ball_size,
paddle_transform.translation, paddle_size):
velocity.value.x = - velocity.value.x
def score():
score = get_resource(Score)
for transform, in Query(Transform,With(Ball)):
if transform.translation.x < 0:
score.right += 1
transform.translation = Vector2(WIDTH/2,HEIGHT/2)
elif transform.translation.x > WIDTH:
score.left += 1
transform.translation = Vector2(WIDTH/2,HEIGHT/2)
def show_score():
score = get_resource(Score)
for text, in Query(Text,With(LeftText)):
text.text = f"{score.left}"
for text, in Query(Text,With(RightText)):
text.text = f"{score.right}"
if __name__ == "__main__":
App() \
.insert_resource(ClearColor(color=pygame.Color('black'))) \
.insert_resource(WindowDescriptor(
title="wonderworld ping",
width=WIDTH,
height=HEIGHT,
resizable=False)) \
.insert_resource(Score(left=0, right=0)) \
.add_plugins(DefaultPlugins()) \
.add_startup_system(setup) \
.add_system(input) \
.add_system(move_ball) \
.add_system(collides) \
.add_system(score) \
.add_system(show_score) \
.run()
pygame 2.1.2 (SDL 2.0.18, Python 3.9.16)
Hello from the pygame community. https://www.pygame.org/contribute.html
- Let's build an Entity Component System from scratch (part 1) https://devlog.hexops.com/2022/lets-build-ecs-part-1/
- Let's build an Entity Component System (part 2): databases https://devlog.hexops.com/2022/lets-build-ecs-part-2-databases/