Demos#

This is a selection of one file demos that show off some of the features of rubato.

Donut#

A spinning donut showing off ease of math.


donut.py
donut.py#
"""
Prototype mathematics in rubato
"""
import rubato as rb
import math

shape: list[tuple[float, float, float]] = []  # list of the points on the shape
roll, pitch, yaw = 0, 0, 0  # x, z, and y counter-clockwise rotation, respectively
surf_dim = 50  # diameter of the surface holding the shape

rb.init(
    (surf_dim, surf_dim),
    (500, 500),
    name="Donut Demo",
    target_fps=60,
)
surf = rb.Surface(surf_dim, surf_dim)

# creates a donut and populates the shape list
rad, thick = 12, 7  # radius and thickness of the donut
mag = rad + thick  # maximum distance of any pt to the origin
for i in range(0, 360, 4):  # revolve around a circle
    for j in range(0, 360, 2):  # revolve the circle around the tube
        v, u = math.radians(i), math.radians(j)
        x = (rad + thick * math.cos(v)) * math.cos(u)
        y = (rad + thick * math.cos(v)) * math.sin(u)
        z = thick * math.sin(v)
        shape.append((x, y, z))


def rotate_pt(x, y, z, roll, pitch, yaw):
    cosp, sinp = math.cos(pitch), math.sin(pitch)
    cosy, siny = math.cos(yaw), math.sin(yaw)
    cosr, sinr = math.cos(roll), math.sin(roll)

    rx = (cosp * siny * sinr - sinp * cosr) * y + (sinp * sinr + cosp * siny * cosr) * z + cosp * cosy * x
    ry = (cosp * cosr + sinp * siny * sinr) * y + (sinp * siny * cosr - cosp * sinr) * z + sinp * cosy * x
    rz = (cosy * sinr) * y + (cosy * cosr) * z - siny * x

    return int(rx), int(ry), int(rz)


def update():
    global roll, pitch, yaw
    roll += 0.0704
    pitch += 0.0352


def draw():
    z_buffer = [-float("inf")] * (surf_dim**2)
    surf.fill(rb.Color.night)
    for point in shape:
        x, y, z = rotate_pt(*point, roll, pitch, yaw)
        if z_buffer[x + surf_dim * y] < z:
            z_buffer[x + surf_dim * y] = z
            color = rb.Color.mix(
                rb.Color.yellow,
                rb.Color.red,
                rb.Math.map(z, -mag, mag, 0, 1),
                "linear",
            )
            surf.set_pixel((x, y), color, False)
    rb.Draw.surface(surf)


rb.Game.update = update
rb.Game.draw = draw
rb.begin()

Physics#

A mosh pit of shapes.


physics_demo.py
physics_demo.py#
"""
A physics demo for rubato
"""
from random import randint
import rubato as rb

# Controls the number of objects in the simulation
num_obj = 60

# Initializes rubato
rb.init(name="rubato physics demo", res=(1980, 1980), window_size=(660, 660))

rb.Game.show_fps = True
# rb.Game.debug = True

main_scene = rb.Scene()  # Create our scene

# Create our four walls
top = rb.GameObject(pos=rb.Display.top_center + rb.Vector(0, 60)).add(
    rb.Rectangle(
        width=rb.Display.res.x + 175,
        height=rb.Display.res.y // 10,
        color=rb.Color.gray,
    )
)

bottom = rb.GameObject(pos=rb.Display.bottom_center + rb.Vector(0, -60)).add(
    rb.Rectangle(
        width=rb.Display.res.x + 175,
        height=rb.Display.res.y // 10,
        color=rb.Color.gray,
    )
)

left = rb.GameObject(pos=rb.Display.center_left + rb.Vector(-60, 0)).add(
    rb.Rectangle(
        width=rb.Display.res.x // 10,
        height=rb.Display.res.y + 175,
        color=rb.Color.gray,
    )
)

right = rb.GameObject(pos=rb.Display.center_right + rb.Vector(60, 0)).add(
    rb.Rectangle(
        width=rb.Display.res.x // 10,
        height=rb.Display.res.y + 175,
        color=rb.Color.gray,
    )
)

# Add the walls to the scene
main_scene.add(top, bottom, left, right)


def should_collide(a, b):
    return a.tag == "" or b.tag == "" or a.tag == b.tag


# Create and add all our objects
for _ in range(num_obj // 2):
    main_scene.add(
        rb.wrap(
            [
                rb.Circle(
                    radius=rb.Display.res.x // num_obj,
                    color=rb.Color.random_default(),
                    tag="circle",
                    should_collide=should_collide,
                ),
                rb.RigidBody(
                    mass=0.1,
                    bounciness=0.99,
                    friction=0.2,
                    gravity=(0, -80),
                    velocity=(randint(-100, 100), randint(-100, 100)),
                ),
            ],
            pos=rb.Display.top_left + (
                randint(int(rb.Display.res.x / 20), int(19 * rb.Display.res.x / 20)),
                -randint(int(rb.Display.res.y / 20), int(19 * rb.Display.res.y / 20)),
            ),
        )
    )
for _ in range(num_obj // 2):
    main_scene.add(
        rb.wrap(
            [
                rb.Polygon(
                    rb.Vector.poly(randint(3, 9), rb.Display.res.x // num_obj),
                    color=rb.Color.random_default(),
                    tag="not_circle",
                    should_collide=should_collide,
                ),
                rb.RigidBody(
                    mass=0.1,
                    bounciness=0.99,
                    friction=0.2,
                    gravity=(0, -80),
                    velocity=(randint(-100, 100), randint(-100, 100)),
                ),
            ],
            pos=rb.Display.top_left + (
                randint(int(rb.Display.res.x / 20), int(19 * rb.Display.res.x / 20)),
                -randint(int(rb.Display.res.y / 20), int(19 * rb.Display.res.y / 20)),
            ),
        )
    )

rb.begin()

Asteroids#

A basic game in rubato showing off hitboxes.


asteroids.py
asteroids.py#
"""
A classic
"""
import random
from rubato import *

size = 1080
half_size = size // 2
radius = size // 40
level = 0

init(name="asteroids", res=(size, size), window_size=(size // 2, size // 2))

main = Scene()

# background
stars = Surface(size, size)
stars.fill(Color.black)
for _ in range(200):
    pos = (
        random.randint(-half_size, half_size),
        random.randint(-half_size, half_size),
    )
    stars.set_pixel(pos, Color.white)


class Timer(Component):

    def __init__(self, secs: float):
        super().__init__()
        self.secs = secs

    def remove(self):
        main.remove(self.gameobj)

    def setup(self):
        Time.delayed_call(self.remove, self.secs)


# explosion particle
expl = Surface(radius // 2, radius // 2)
expl.draw_rect((0, 0), expl.size(), Color.debug, 3)


def make_part(angle: float):
    return Particle(
        expl.clone(),
        pos=Particle.circle_shape(radius * 0.75)(angle),
        velocity=Particle.circle_direction()(angle) * random.randint(50, 100),
        rotation=random.randint(0, 360),
    )


# explosion system
expl_sys = wrap([
    ParticleSystem(new_particle=make_part, mode=ParticleSystemMode.BURST),
    Timer(5),
])


# component to move things that are out of bounds
class BoundsChecker(Component):

    def update(self):
        if self.gameobj.pos.x < Display.left - radius:
            self.gameobj.pos.x = Display.right + radius
        elif self.gameobj.pos.x > Display.right + radius:
            self.gameobj.pos.x = Display.left - radius
        if self.gameobj.pos.y > Display.top + radius:
            self.gameobj.pos.y = Display.bottom - radius
        elif self.gameobj.pos.y < Display.bottom - radius:
            self.gameobj.pos.y = Display.top + radius


# asteroid generator
def make_asteroid():
    sides = random.randint(5, 8)

    t = random.randint(-half_size, half_size)
    topbottom = random.randint(0, 1)
    side = random.randint(0, 1)
    if topbottom:
        pos = t, Display.top + (side * size + (radius if side else -radius))
    else:
        pos = Display.left + (side * size + (radius if side else -radius)), t

    direction = (-Display.center.dir_to(pos)).rotate(random.randint(-45, 45))

    main.add(
        wrap(
            [
                Polygon(
                    [
                        Vector.from_radial(
                            random.randint(
                                int(radius * .7),
                                int(radius * 0.95),
                            ),
                            -i * 360 / sides,
                        ) for i in range(sides)
                    ],
                    debug=True,
                ),
                RigidBody(
                    velocity=direction * 100,
                    ang_vel=random.randint(-30, 30),
                ),
                BoundsChecker(),
            ],
            pos=pos,
            rotation=random.randint(0, 360),
            name="asteroid",
        )
    )


Time.schedule(RecurrentTask(make_asteroid, 1, 1))


class PlayerController(Component):

    def setup(self):
        self.speed = 250
        self.steer = 25

        self.velocity = Vector()
        self.interval = .2
        self.allowed_to_shoot = True
        self.gameobj.add(BoundsChecker())

    def update(self):
        controller_pressed = Input.controllers() and Input.controller_button(0, 0)
        if controller_pressed or Input.key_pressed("j") or Input.key_pressed("space"):
            self.shoot()

    def shoot(self):
        if self.allowed_to_shoot:
            main.add(
                wrap(
                    [
                        Circle(
                            radius // 5,
                            Color.debug,
                            trigger=True,
                            on_collide=bullet_collide,
                        ),
                        RigidBody(
                            velocity=self.gameobj.get(PlayerController).velocity \
                                + Vector.from_radial(
                                    500,
                                    self.gameobj.rotation,
                                )
                        ),
                        BoundsChecker(),
                        Timer(0.75),
                    ],
                    pos=self.gameobj.pos,
                    rotation=self.gameobj.rotation,
                    name="bullet",
                )
            )
            self.allowed_to_shoot = False
            Time.delayed_call(
                lambda: setattr(self, "allowed_to_shoot", True),
                self.interval,
            )

    def fixed_update(self):
        c_axis_0 = Input.controller_axis(0, 0) if Input.controllers() else 0
        c_axis_1 = -Input.controller_axis(0, 1) if Input.controllers() else 0
        c_axis_0 = 0 if Input.axis_centered(c_axis_0) else c_axis_0
        c_axis_1 = 0 if Input.axis_centered(c_axis_1) else c_axis_1
        dx = c_axis_0 or \
            (-1 if Input.key_pressed("a") or Input.key_pressed("left") else (1 if Input.key_pressed("d") or Input.key_pressed("right") else 0))
        dy = c_axis_1 or \
            (1 if Input.key_pressed("w") or Input.key_pressed("up") else (-1 if Input.key_pressed("s") or Input.key_pressed("down") else 0))
        target = Vector(dx, dy)

        d_vel = target * self.speed
        steering = Vector.clamp_magnitude(d_vel - self.velocity, self.steer)

        self.velocity = Vector.clamp_magnitude(self.velocity + steering, self.speed)

        self.gameobj.pos += self.velocity * Time.fixed_delta

        if target != (0, 0):
            self.gameobj.rotation = self.velocity.angle


# player geometry, we cannot have concave polygons (yet), this gets the absolute hitbox.
full = [
    Vector.from_radial(radius, 0),
    Vector.from_radial(radius, 125),
    Vector.from_radial(radius // 4, 180),
    Vector.from_radial(radius, -125),
]
right = [full[0], full[1], full[2]]
left = [full[0], full[2], full[3]]
player_spr = Raster(radius * 2, radius * 2)
player_spr.draw_poly(full, (0, 0), Color.debug, 2, aa=True)

main.add(
    wrap(
        [
            PlayerController(),
            Polygon(right, trigger=True),
            Polygon(left, trigger=True),
            player_spr,
        ],
        pos=Display.center,
        name="player",
    )
)


def bullet_collide(man: Manifold):
    if man.shape_b.gameobj.name == "asteroid":
        local_expl = expl_sys.clone()
        local_expl.pos = man.shape_b.gameobj.pos.clone()
        local_expl.rotation = random.randint(0, 360)
        local_expl_sys = local_expl.get(ParticleSystem)
        if isinstance(man.shape_b, Polygon):
            local_expl_sys.spread = 360 / len(man.shape_b.verts)
        local_expl_sys.start()
        main.remove(man.shape_b.gameobj)
        main.remove(man.shape_a.gameobj)
        main.add(local_expl)


def new_draw():
    Draw.surface(stars, Display.center)


Game.draw = new_draw

begin()

Offset#

A showcase of the relative offsets of GameObjects and Components.


offset_demo.py
offset_demo.py#
"""
A demo to demonstrate how offset and rotation play together.
"""
import rubato as rb
from rubato import Vector as V

width, height = 256, 256
speed = 2

rb.init(res=V(width, height), window_size=V(width, height) * 2)
s = rb.Scene()

rect = rb.Polygon(V.poly(5, width // 6), rb.Color.blue, offset=V(48, 0))
go = rb.wrap(rect, pos=rb.Display.center, debug=True)

dropper = rb.Rectangle(width=20, height=20, color=rb.Color.red, debug=True)
rigidbody = rb.RigidBody(gravity=V(0, -100))
extra = rb.wrap([dropper, rigidbody])

font = rb.Font()
font.size = 10
text = rb.Text("Hello World", font)


def update():
    go.rotation += speed
    rect.rot_offset += speed

    text.text = f"go.rotation: {go.rotation:.2f}\nrect.offset.x: {rect.offset.x:.2f}\nrect.rot_offset: {rect.rot_offset:.2f}"


def handler(m_event):
    if m_event["button"] == 1:
        e = extra.clone()
        e.pos = V(m_event["x"], m_event["y"])
        s.add(e)
    elif m_event["button"] == 3:
        rect._image.save_as("save_test", "test", "jpg", quality=90)


rb.Radio.listen(rb.Events.MOUSEDOWN, handler)

s.add(go, rb.wrap(text, pos=rb.Display.top_left + (50, -20)))
s.fixed_update = update

rb.begin()

Sound#

A demo of a beautiful voice showing how easy it is to use the sound system.


sound_demo.py
sound_demo.py#
"""
A sound demo for rubato
"""
import rubato as rb

rb.init(
    name="Sound Test",
    window_size=rb.Vector(300, 0),
    res=rb.Vector(0, 0),
)

main_scene = rb.Scene()

# Import the sound folder shallowly
rb.Sound.import_sound_folder("sounds", recursive=False)

click = rb.Sound.get_sound("click")  # Get sound instance
music = rb.Sound.get_sound("music")

# player 1 and 2 have duplicate file names so the absolute path is used as a key
rb.Sound.import_sound_folder("sounds/player1", True)
rb.Sound.import_sound_folder("sounds/player2", True)

player1_intro = rb.Sound.get_sound("sounds/player1/intro")
player1_intro.play()

rb.Time.delayed_call(rb.Sound.get_sound("sounds/player2/intro").play, 0.65)


def update():
    if rb.Input.key_pressed("space"):
        click.play(0)


def input_listener(keyinfo):
    if keyinfo["key"] == "m":
        music.play()
    if keyinfo["key"] == "a":
        click.play(20)
    if keyinfo["key"] == "s":
        click.stop()
        music.stop()
    if keyinfo["key"] == "p":
        if click.state == rb.Sound.PLAYING:
            click.pause()
        elif click.state == rb.Sound.PAUSED:
            click.resume()
        if music.state == rb.Sound.PLAYING:
            music.pause()
        elif music.state == rb.Sound.PAUSED:
            music.resume()
    if keyinfo["key"] == "up":
        click.set_volume(click.get_volume() + 10)
        print(f"Volume UP: {click.get_volume()}")
    if keyinfo["key"] == "down":
        click.set_volume(click.get_volume() - 10)
        print(f"Volume DOWN: {click.get_volume()}")


rb.Radio.listen("KEYDOWN", input_listener)

main_scene.update = update

rb.begin()

Surfaces#

A rubato surface drawing function demonstration.


surface_demo.py
surface_demo.py#
"""
Demonstrating how to use surfaces in rubato.
"""
from rubato import init, begin, Draw, Display, Surface, Game, Vector as V, Color

width, height = 32, 32
gridx, gridy = 4, 4
main_c = Color.red
second_c = Color.green
bg_c = Color.blue
init(V(width * gridx, height * gridy), V(640, 640), name="Surface Demo")

shapes = [Surface(width, height) for _ in range(gridx * gridy)]

for shape in shapes:
    shape.draw_rect(V(0, 0), V(width, height), fill=bg_c)

col = 0

line_p = (V(-12, 9), V(12, -9))

shapes[col].draw_line(*line_p, main_c)
shapes[gridx + col].draw_line(*line_p, main_c, True)
shapes[2 * gridx + col].draw_line(*line_p, main_c, thickness=3)
shapes[3 * gridx + col].draw_line(*line_p, main_c, True, 2, True)

col += 1

rect_d = (V(0, 0), V(width - 8, height - 8))

shapes[col].draw_rect(*rect_d, main_c)
shapes[gridx + col].draw_rect(*rect_d, main_c)
shapes[2 * gridx + col].draw_rect(*rect_d, main_c, 3, second_c)
shapes[3 * gridx + col].draw_rect(*rect_d, main_c, 2, second_c, True)

col += 1

circle_d = (V(0, 0), (width // 2) - 2)

shapes[col].draw_circle(*circle_d, main_c)
shapes[gridx + col].draw_circle(*circle_d, main_c, aa=True)
shapes[2 * gridx + col].draw_circle(*circle_d, main_c, 3, second_c)
shapes[3 * gridx + col].draw_circle(*circle_d, main_c, 2, second_c, True)

col += 1

points = ([v for v in V.poly(6, (width / 2) - 2)], (0, 0))

shapes[col].draw_poly(*points, main_c)
shapes[gridx + col].draw_poly(*points, main_c, aa=True)
shapes[2 * gridx + col].draw_poly(*points, main_c, 3, second_c)
shapes[3 * gridx + col].draw_poly(*points, main_c, 2, second_c, True)


def update():
    for i in range(len(shapes)):
        Draw.queue_surface(
            shapes[i],
            Display.top_left + (
                (i % gridx) * width + (width / 2),
                -(i // gridx) * height - (height / 2),
            ),
        )


Game.update = update

begin()