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
"""
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
"""
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
"""
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
"""
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
"""
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
"""
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()