This document describes the current public API of the engine as it exists in this repository. It is intentionally based on the actual code and package exports, not an older draft.
Get a WhaleEngine installer
Move it into your project folder.
Run it:
MacOS/Linux:
chmod u+x WEInstaller-MacLin.sh
./WEInstaller-MacLin.shWindows:
.\WEInstaller-Win.batInstall WindowAPI-s you wan't.
And there you go.
Now you can use WhaleEngine.
from WhaleEngine import *
from WhaleEngine.WindowAPI.OpenGL import windowAPI # from WhaleEngine.WindowAPI.Vulkan import windowAPI # from WhaleEngine.WindowAPI.WebGL import windowAPI
window = windowAPI(title="Whale engine app") # create a window using the OpenGL API, you can also use Vulkan or WebGL by changing the import above and uncommenting the line below
app = WhaleEngine(window=window) # create app with the window we just made
renderer = Renderer2D() # create a 2D renderer
app.input = InputSystem() # loads input system so you can use app.input instead of app.InputSystem
shapes = LoadShapes() # load built in shapes
textures = LoadTextures() # load built textures
window.set_color(Color.white) # set window background color to white
entity = Entity2D(texture=textures.whale) # create an entity with the whale texture
def update(dt):
if app.input.key(Keys.SPACE): # check if space is being pressed
entity.rotation += 90 * dt # rotate entity 90 degrees per second
entity.x -= 100 * dt # move entity 100 pixels to the right per second
app.update = update # set the app's update function to the one we just made
def on_app_close():
logLn("Closing", "app") # logs this: <app> Closing
app.on_app_close = on_app_close # set the app's on_app_close function
app.run() # run the app
# things you write here after app.run() won't be ever executedYou create the OS/window layer through one of the backend modules:
from WhaleEngine.WindowAPI.OpenGL import windowAPI
# or: from WhaleEngine.WindowAPI.Vulkan import windowAPI
# or: from WhaleEngine.WindowAPI.WebGL import windowAPI
window = windowAPI(
title="Whale engine app",
width=800,
height=600,
color=Color.dark_gray,
)Common methods:
window.set_size(width, height)
window.set_width(width)
window.set_height(height)
window.set_title(title)
window.set_color(color)app = WhaleEngine(window=window)Important attributes and behavior:
app.update = update_function # called every frame with dt
app.on_app_close = callback # executed before shutdown
app.clamping = False # enable dt clamping
app.clamping_threshold = 0.1 # max dt when clamping is onLifecycle methods:
app.run()
app.close_app()
app.exit() # alias to close_app
app.close() # alias to close_appThe engine runs a main loop while window.should_close() is false.
renderer = Renderer2D()The renderer is automatically added to current_app.renderers when created. It keeps a list of entities and calls render() on the window when ready.
renderer.start() # no-op by default
renderer.update(dt) # custom logic hook
renderer.add(entity) # add an entity manuallyEach renderer has a camera:
renderer.camera.x = 0
renderer.camera.y = 0
renderer.camera.zoom = 1
renderer.camera.rotation = 0entity = Entity2D(
texture=Texture("path/to/texture.png"),
color=Color.white,
position=(0, 0),
scale=(1, 1),
rotation=0.0,
update=False,
renderer=0,
visible=True,
enabled=True,
shader=None,
)Fields:
entity.x
entity.y
entity.rotation
entity.scale_x
entity.scale_y
entity.visible
entity.enabled
entity.colorUseful helpers:
entity.get_position() # returns (x, y)
entity.set_position((x, y))The update argument is a boolean. If set to True, the entity's update(dt) method is used each frame; otherwise it is skipped.
from WhaleEngine import Text2D
text = Text2D(
"Hello",
font_path="arial.ttf", # can be left empty
font_size=32,
color=Color.white,
position=(0, 0),
)Useful methods:
text.set_text("New text")
text.set_font_size(40)from WhaleEngine import Line2D
line = Line2D(start=(0, 0), end=(200, 0), color=Color.red, scale=1)texture = Texture("path/to/image.png")Notes:
relative=Trueis the default. The path is resolved relative to the project root.- If the file cannot be loaded, the engine falls back to the built-in missing texture asset.
- You can create a texture from a Pillow image:
from PIL import Image
image = Image.new("RGBA", (64, 64), (255, 0, 0, 255))
texture = Texture.from_image(image)Texture attributes:
texture.w
texture.h
texture.path
texture.idcolor = Color(r, g, b, a) # values are 0.0 - 1.0Static constructors:
Color.rgb(r, g, b) # 0-255 input, alpha = 1
Color.rgba(r, g, b, a) # 0-255 input
Color.hsv(h, s, v) # h/s/v in 0.0-1.0
Color.hex("#ff8800") # hex stringCommon presets:
Color.white
Color.black
Color.red
Color.green
Color.blue
Color.yellow
Color.magenta
Color.cyan
Color.orange
Color.purple
Color.gray
Color.pink
Color.brown
Color.lime
Color.navy
Color.teal
Color.gold
Color.crimsonThe engine includes a few default texture/sound assets.
shapes = LoadShapes()
textures = LoadTextures()
sounds = LoadSounds()Example access:
shapes.square
shapes.circle
shapes.triangle
shapes.dot
shapes.star
shapes.arrow
textures.dodo
textures.whale
textures.old_whale
textures.grid
textures.missing_texture
textures.placeholder
sounds.music
sounds.soundInput should be attached to the app before you use it:
app.input = InputSystem()Use it like this:
app.input.key(Keys.A)
app.input.key_pressed(Keys.A)
app.input.key_released(Keys.A)if you don't attach it to the app then it's use looks like this:
app.InputSystem,key(Keys.A)
#...Common keys:
Keys.A, Keys.W, Keys.S, Keys.D
Keys.UP, Keys.DOWN, Keys.LEFT, Keys.RIGHT
Keys.SPACE, Keys.ESCAPE, Keys.ENTER
Keys.F1, Keys.F2, ..., Keys.F12Example:
def update(dt):
if app.input.key_pressed(Keys.ESCAPE):
app.exit()mouse = MouseSystem()Mouse state:
mouse.x
mouse.y
mouse.wx
mouse.wy
mouse.left_down
mouse.right_downMethods:
mouse.get_position() # returns (x, y)
mouse.set_position(x, y)
mouse.left_pressed()
mouse.right_pressed()It is important not to create a new MouseSystem() instance repeatedly; store it in a variable once.
The sound system is a plugin and must be available before creating Sound objects.
app.SoundSystem = SoundSystem()Or if you are using plugin auto-registration patterns, the engine expects the SoundSystem plugin to be present in the app.
Loading and playing sounds:
sound = Sound("name", "path/to/sound.mp3")
sound.play()
sound.play(loops=3)
sound.play(loops=-1)
sound.stop()
sound.set_volume(0.5)
sound.get_volume()The Sound object exposes:
sound.is_playing
sound.volume- The project is still evolving and some APIs are experimental.
- The OpenGL backend is the most stable choice.
- The README and documentation may lag behind the code in some parts, so prefer checking the source files when needed.
from WhaleEngine import *
from WhaleEngine.WindowAPI.OpenGL import windowAPI
window = windowAPI(title="My App")
app = WhaleEngine(window=window)
app.input = InputSystem()
renderer = Renderer2D()
textures = LoadTextures()
player = Entity2D(texture=textures.dodo, position=(0, 0))
def update(dt):
if app.input.key_pressed(Keys.ESCAPE):
app.exit()
if app.input.key(Keys.A):
player.x -= 100 * dt
if app.input.key(Keys.D):
player.x += 100 * dt
app.update = update
app.run()The engine supports per-entity custom shaders through the OpenGL shader system in WhaleEngine/WindowAPI/OpenGL/shader.py.
from WhaleEngine.WindowAPI.OpenGL.shader import Shader
shader = Shader(fragment_code="""
#version 330 core
out vec4 FragColor;
void main() {
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
""")The Shader class can:
- compile a fragment shader and optional vertex shader
- bind itself with
shader.use() - set uniforms with
set_mat4(),set_vec4(),set_int(),set_float() - be created from a file via
Shader.from_file(path, vertex_path=None)
You can assign a shader directly to an entity:
entity = Entity2D(
texture=textures.dodo,
shader=shader,
)The engine will use that shader when rendering the entity, unless you omit it and it falls back to the default built-in shader.
The common bundled shaders are defined in WhaleEngine/WindowAPI/OpenGL/shaders.py:
from WhaleEngine.WindowAPI.OpenGL.shaders import normal, grayscale, invert, sepia, vignette, outline, brighten
entity.shader = grayscale
entity.shader = invert
entity.shader = sepia
entity.shader = vignette
entity.shader = outline
entity.shader = brightenAvailable presets:
normal
grayscale
invert
sepia
vignette
outline
brightenThese are precompiled fragment shader variants with the default vertex shader.
These are the extra runtime classes that live directly in the package and are often used by larger projects.
BetterRenderer2D extends Renderer2D and only renders entities that are visible and currently on screen:
from WhaleEngine import BetterRenderer2D
renderer = BetterRenderer2D()It uses is_on_screen2D(entity, self.camera) before drawing, so it is more efficient for large worlds.
cam = renderer.camera
cam.x = 0
cam.y = 0
cam.zoom = 1.0
cam.rotation = 0Methods:
cam.get_position() # returns (x, y)
cam.set_position((x, y))Base class for custom engine plugins:
from WhaleEngine import Plugin
class MyPlugin(Plugin):
def __init__(self):
super().__init__(requirements=[], incompatibilities=[])
def update(self, dt):
passEvery plugin is assigned to app.plugins[name] and also becomes an attribute on the app instance.
from WhaleEngine import TimerSystem, Timer, delay
app.TimerSystem = TimerSystem()
t = Timer(2.5)
print(t.over) # False until time has elapsed
t.reset()
delay(1.0, func=lambda: print("done"))Timer properties:
t.time
t.lenght
t.over
t.reset()Sync attributes from a parent object to a child object:
from WhaleEngine import ParentIn, ParentingSystem
app.ParentingSystem = ParentingSystem()
ParentIn(parent, child, attributes={"x": "set", "y": "set"})Available modes:
"set"— child property is set directly to parent value"add"— child property is adjusted by the difference between parent values
from WhaleEngine import QuadCollider2D
collider = QuadCollider2D(
w=100,
h=80,
position=(0, 0),
rotation=0,
layers=[0],
visualize=False,
)Important members:
collider.x
collider.y
collider.w
collider.h
collider.rotation
collider.layers
collider.enabled
collider.colliding
collider.get_position()
collider.set_position((x, y))
collider.ignore(other_collider)This collider requires BetterCollisionSystem2D to be active.
from WhaleEngine import MeshCollider2D, Texture
collider = MeshCollider2D(
shape=Texture("path/to/texture.png"),
density=16,
position=(0, 0),
scale=(1, 1),
rotation=0,
layers=[0],
visualize=False,
)This creates a collision polygon from a texture mask and is also handled by BetterCollisionSystem2D.
from WhaleEngine import BetterCollisionSystem2D
app.BetterCollisionSystem2D = BetterCollisionSystem2D()This system tracks all quad and mesh colliders and updates their colliding state every frame.
from WhaleEngine import CircleCollider2D
collider = CircleCollider2D(
size=100,
position=(0, 0),
layers=[0],
visualize=False,
)Members:
collider.x
collider.y
collider.size
collider.layers
collider.colliding
collider.enabled
collider.get_position()
collider.set_position((x, y))
collider.ignore(other_collider)Requires CircleCollisionSystem2D.
from WhaleEngine import MeshCircleCollider2D
mesh = MeshCircleCollider2D(
shape=Texture("path/to/texture.png"),
density=8,
size=8,
position=(0, 0),
visualize=False,
)This creates several circle colliders from a texture, then links them to the circle collision system.
from WhaleEngine import CircleCollisionSystem2D
app.CircleCollisionSystem2D = CircleCollisionSystem2D()This system handles circle and mesh-circle collision detection.
from WhaleEngine import ParticleSystem2d, ParticleType2d, Particle2d, ParticleSpawner2d, Range
app.ParticleSystem2d = ParticleSystem2d()Example particle type:
ptype = ParticleType2d(
texture=shapes.star,
lifetime=Range(1, 3),
x_speed=Range(-50, 50),
y_speed=Range(20, 80),
rotation_speed=Range(-180, 180),
scale_x=Range(0.5, 1.0),
scale_y=Range(0.5, 1.0),
color_r=Range(0, 255),
color_g=Range(0, 255),
color_b=Range(0, 255),
color_a_speed=Range(-0.5, -0.1),
)Spawn a single particle:
Particle2d(ptype, x=0, y=0)Spawn continuously:
spawner = ParticleSpawner2d(ptype, x=0, y=0, spawn_rate=30)
spawner.active = Truefrom WhaleEngine import Range
r = Range(1, 10)
value = r.safe_uniform()Examples:
Range(5) # always 5
Range(0, 100) # random between 0 and 100from WhaleEngine import Button2D, checkbox, Color, Texture
button = Button2D(
texture=Texture("path/to/button.png"),
color=Color.white,
position=(0, 0),
onclick=lambda: print("clicked"),
onpress=lambda: print("pressed"),
)
check = checkbox(checked=False, position=(100, 0))These rely on BetterCollisionSystem2D, ParentingSystem, and MouseSystem.
from WhaleEngine import ConversationRenderer
conversation = ConversationRenderer(text_color=Color.white, backround_color=Color.black)It is a specialized renderer for dialogue boxes and text blocks.
The logging helpers are in WhaleEngine/logging.py.
from WhaleEngine.logging import logLn, set_logging_file, set_logging_folder
logLn("message")
logLn("message", "MyPlugin")
set_logging_file("logs/app.log")
set_logging_folder("logs")Behavior:
logLn(message, by="WhaleEngine")prints to console- if a log file or folder is configured, it also writes to disk
set_logging_file(path)creates one file for that runset_logging_folder(path)creates a unique log file per execution
The raycast helper is in WhaleEngine/raycast2d.py.
from WhaleEngine.raycast2d import raycast2d
hit = raycast2d(start=(0, 0), end=(500, 0))
hit = raycast2d(start=(0, 0), end=(500, 0), layers=[0])Returns:
(x, y)hit point when an object is hitNonewhen nothing is hit- it checks both circle colliders and polygon-based colliders if those systems exist
The generic helpers live in WhaleEngine/utils.py.
from WhaleEngine.utils import Range, safe_uniform, layers_match, pixel_is_solid
rng = Range(0, 100)
value = rng.safe_uniform()Useful functions:
pixel_is_solid(r, g, b, a, alpha_threshold=10)
layers_match(a, b) # checks if two colliders share a layer
safe_uniform(a, b)Range is used by particle systems and other randomized values:
Range(5) # fixed value
Range(1, 10) # random float between 1 and 10
Range(10, 5) # also works in reverse orderThe geometry helpers are in WhaleEngine/utils2d.py.
from WhaleEngine.utils2d import distance2D, distance2D_points, angle_to2D, forwardPos2D, forwardMove2D, is_on_screen2D
length = distance2D(entity_a, entity_b)
angle = angle_to2D((0, 0), (100, 50))
next_pos = forwardPos2D((0, 0), 90, 50)Common functions:
distance2D(entity_a, entity_b)
distance2D_points((x1, y1), (x2, y2))
angle_to2D(pos1, pos2)
forwardPos2D(pos, angle, distance)
forwardMove2D(angle, distance)
is_on_screen2D(entity, camera)The small helper utilities in WhaleEngine/helpers/init.py are used for convenience and default values:
from WhaleEngine.helpers import default, none, And, Or
value = none()These are lightweight helpers:
none(value=None)returnsNonedefaultis a placeholder/default class used in UI assets and optional fieldsAnd(a, b)andOr(a, b)are basic logical wrappers
The prefabs folder contains reusable, higher-level game objects. The current one included in the project is WhaleEngine/prefabs/charactercontroller2d.py.
from WhaleEngine.prefabs.charactercontroller2d import CharacterController2D
player = CharacterController2D(
texture=textures.dodo,
collider_w=60,
collider_h=80,
feetray_lenght=20,
position=(0, 0),
)Behavior:
- handles movement with
A/D - handles jump with
W - requires
BetterCollisionSystem2DandInputSystem - uses a raycast downwards to detect grounded state
- tracks
grounded,y_velocity, andjump_requested
Also look at examples/ folder for practical examples.