Kaptan is a small 2D game engine written in Odin. Games are scripted in Lua and can use Raylib rendering/audio, Box2D physics, input, timers, coroutines, animation curves, and lightweight spatial queries.
Kaptan is still evolving. The examples and API reference in this README describe the current Lua API.
Build and run the default Lua script:
./build.shRun a windowed example:
./build.sh debug tests/window/main.luabuild.sh writes build/kaptan and passes the Lua script path after --. Paths in examples use assets from this repository, such as tests/sprites/kaptan1.png.
KaptanWindow.open('Kaptan', 800, 600)
local layer = KaptanLayer.new()
KaptanRenderer.add(layer)
local sprite = KaptanSprite.new('tests/sprites/kaptan1.png')
layer:add(sprite)
KaptanWindow.setLoopCallback(function()
local dt = KaptanWindow.getDeltaTime()
sprite:setRot(sprite:getRot() + 90 * dt)
end)KaptanWindow.open creates the Raylib window. KaptanLayer stores render items, KaptanRenderer.add(layer) makes the layer visible, and KaptanWindow.setLoopCallback runs once per frame.
Kaptan uses a center-relative 2D rendering model. Object positions are not top-left screen coordinates. By default, {0, 0} means the center of the current render space.
For normal world layers, the render space is controlled by KaptanCamera. For GUI layers, the render space is the viewport itself, but still center-relative.
Positive x moves right. Positive y moves down.
KaptanWindow.open(title, width, height) creates the OS window and sets the default viewport to the same size. KaptanWindow.getWidth() and KaptanWindow.getHeight() return the window size. KaptanWindow.getViewportSize() returns the logical render size used by cameras, detached GUI layers, and mouse picking.
Set a fixed logical viewport with KaptanWindow.setViewportSize(width, height). When the viewport differs from the window, Kaptan renders the frame to a texture at the viewport size and stretches it to fill the whole window. Aspect ratio is not preserved.
KaptanWindow.open('Kaptan', 1280, 720)
KaptanWindow.setViewportSize(640, 360) -- logical render space, stretched to the window
local viewport_width, viewport_height = KaptanWindow.getViewportSize()Use viewport dimensions for game and GUI layout. Use window dimensions only when you specifically need the actual OS window size.
KaptanWindow.setFullscreen(enabled) toggles fullscreen presentation for the current window.
Viewport size is independent from fullscreen size. If you set a fixed viewport, Kaptan renders at that logical resolution and stretches it to the fullscreen window:
KaptanWindow.open('Kaptan', 1280, 720)
KaptanWindow.setViewportSize(640, 360)
KaptanWindow.setFullscreen(true)If you do not call setViewportSize, the viewport follows the window size, including after fullscreen toggles.
Use KaptanWindow.showMouseCursor(visible) to show or hide the OS mouse cursor. This only changes cursor visibility; mouse position, buttons, and wheel input continue to work.
KaptanWindow.showMouseCursor(false)Layers are render containers. A layer can be attached to the camera or detached from it.
Camera-attached layers are the default:
world = KaptanLayer.new()
KaptanRenderer.add(world)
print(world:isCamAttached()) -- trueA camera-attached layer renders through KaptanCamera. Moving, rotating, or zooming the camera affects every sprite, shape, or text object in that layer.
Detached layers are useful for GUI and HUD rendering:
hud = KaptanLayer.new()
hud:setCamAttached(false)
KaptanRenderer.add(hud)A detached layer ignores KaptanCamera. It uses a viewport-space camera where {0, 0} is the viewport center. This keeps HUD elements fixed on screen while the world camera moves.
Layer order is preserved. If you add a world layer first and a HUD layer second, the HUD renders on top:
world = KaptanLayer.new()
hud = KaptanLayer.new()
hud:setCamAttached(false)
KaptanRenderer.add(world)
KaptanRenderer.add(hud)The default camera maps world {0, 0} to the viewport center.
KaptanCamera.setPos(0, 0)
KaptanCamera.setPiv(0, 0)
KaptanCamera.setZoom(1)
KaptanCamera.setRot(0)KaptanCamera.setPos(x, y) chooses the world point the camera looks at. With the default pivot, that world point appears at the viewport center.
KaptanCamera.setPos(300, 200) -- put world point {300, 200} at the viewport centerKaptanCamera.setPiv(x, y) moves where the camera target appears on screen, relative to the viewport center.
KaptanCamera.setPiv(100, 0) -- keep the camera target 100 pixels to the right of viewport centerKaptanCamera.setZoom(zoom) zooms camera-attached layers only. Detached GUI layers are not zoomed.
KaptanCamera.setZoom(2) -- world appears 2x larger, HUD stays unchangedSprites, N-patches, draw shapes, text, text boxes, and groups share common transform, color, and visibility methods:
setPos(x, y)andgetPos()setPiv(x, y)andgetPiv()setRot(angle)andgetRot()setScl(x, y)andgetScl()setColor(r, g, b, a)andgetColor()setVisible(visible)andisVisible()
Use these methods to move, rotate, scale, tint, or hide render items without changing which layer or group owns them.
setPos places an object's pivot point. For sprites, N-patches, text, and text boxes, the default pivot is {0, 0}, meaning the visual center, so setPos(0, 0) centers the object in the active render space. Groups have no intrinsic size, so their pivot is their local transform origin.
sprite = KaptanSprite.new('tests/sprites/kaptan1.png')
sprite:setPos(0, 0) -- sprite center at world/viewport center
world:add(sprite)For a GUI layer, the same position means viewport center and does not move with the camera:
hud = KaptanLayer.new()
hud:setCamAttached(false)
KaptanRenderer.add(hud)
label = KaptanText.new('tests/text/unitblock.ttf', 'Hello', 72)
label:setPos(0, 0) -- centered on screen
hud:add(label)Because positions are center-relative, top-left placement uses half the viewport dimensions. Set the object pivot to its top-left corner, then place that pivot at the desired viewport-relative point.
For a sprite with size {sprite_width, sprite_height}, place its top-left corner at screen top-left:
viewport_width, viewport_height = KaptanWindow.getViewportSize()
sprite_width, sprite_height = sprite:getSize()
sprite:setPiv(-sprite_width / 2, -sprite_height / 2)
sprite:setPos(
-viewport_width / 2,
-viewport_height / 2
)Text, text boxes, and N-patches use the same idea. For text, getSize() returns the measured unscaled text size:
viewport_width, viewport_height = KaptanWindow.getViewportSize()
text_width, text_height = label:getSize()
label:setPiv(-text_width / 2, -text_height / 2)
label:setPos(
-viewport_width / 2,
-viewport_height / 2
)This works naturally for detached GUI layers because {0, 0} is the viewport center. For camera-attached world layers, the same formula places the pivot relative to the camera's current view, not permanent world top-left.
The pivot controls what point setPos places and where rotation and scaling happen from. The default pivot is {0, 0}, meaning the visual center for sprites, N-patches, text, and text boxes.
Center pivot:
sprite:setPiv(0, 0)
sprite:setRot(45) -- rotates around sprite center
sprite:setScl(1.5, 1.5) -- scales outward from sprite centerTop-left pivot for a sprite:
sprite_width, sprite_height = sprite:getSize()
sprite:setPiv(-sprite_width / 2, -sprite_height / 2)
sprite:setPos(0, 0) -- sprite top-left at world/viewport center
sprite:setRot(45) -- rotates around sprite top-left
sprite:setScl(1.5, 1.5) -- top-left remains fixed while the sprite grows down/rightTop-left pivot for text:
text_width, text_height = label:getSize()
label:setPiv(-text_width / 2, -text_height / 2)
label:setPos(0, 0) -- text top-left at world/viewport center
label:setRot(45)
label:setScl(1.5, 1.5)Transformable render items support two-axis scaling:
sprite:setScl(2, 1) -- twice as wide, same height
shape:setScl(1, 2) -- same width, twice as tall
label:setScl(2, 2) -- text appears 2x larger uniformly
label:setScl(2, 1) -- text twice as wide, same height
box:setScl(1, 2) -- text box same width, twice as tallText and text boxes render directly in normal translation, rotation, and uniform-scale cases for crisp glyphs and stable layout. Complex inherited transforms that cannot be represented by Raylib text drawing, such as non-uniform scale mixed with rotation, use a cached texture fallback so group transforms still compose correctly. Kaptan applies bilinear filtering to loaded font atlases so rotated or scaled glyphs have smoother edges than point-filtered text.
For large text, prefer loading the font at the intended size instead of scaling small text up heavily:
title = KaptanText.new('tests/text/unitblock.ttf', 'Title', 72) -- preferred
small = KaptanText.new('tests/text/unitblock.ttf', 'Title', 24)
small:setScl(3, 3) -- works, but can look softer or blockierVery large scale factors can still reveal the source font-atlas resolution, and rotated text can still show some sampling artifacts.
Rotation is in degrees. Positive rotation follows Raylib's 2D rotation direction.
sprite:setRot(30)
shape:setRot(30)
label:setRot(30)Rotation happens around the pivot. Use setPiv when you need a corner or custom anchor point.
Sprites can render a trimmed source rectangle from a larger texture atlas while keeping logical frame size and pivot behavior from the original untrimmed sprite.
Kaptan includes a TexturePacker custom exporter in extra/TexturePacker/kaptan. See the TexturePacker custom exporter documentation for installation and usage details: https://www.codeandweb.com/texturepacker/documentation/custom-exporter.
The included exporter emits a .lua file using the format below and disables rotated sprites because Kaptan's sprite renderer does not support atlas rotation.
A Lua spritesheet can keep all metadata in pixels:
return {
texture = 'sheet.png',
sprites = {
kaptan1 = {
source = { x = 2, y = 2, w = 150, h = 100 },
frame = { w = 160, h = 110 },
offset = { x = 5, y = 5 },
},
}
}sourceis the pixel rectangle sampled from the atlas texture.frameis the original untrimmed logical sprite size.offsetis where the trimmed visible pixels start inside the untrimmed frame, measured from the frame top-left.
Use the metadata when constructing a sprite:
local sheet = dofile('tests/spritesheet/sheet.lua')
local data = sheet.sprites.kaptan1
local sprite = KaptanSprite.new('tests/spritesheet/' .. sheet.texture)
sprite:setFrame(data)sprite:getSize() returns the logical frame size, not the trimmed source size. This keeps placement and pivot formulas consistent:
local w, h = sprite:getSize()
sprite:setPiv(-w / 2, -h / 2) -- logical frame top-leftUse KaptanNPatch for scalable UI panels and buttons. It wraps Raylib's n-patch drawing and supports 9-patch, vertical 3-patch, and horizontal 3-patch layouts.
local panel = KaptanNPatch.new('tests/ui/ui-sheet.png')
panel:setSourceRect(0, 0, 32, 32)
panel:setBorders(8, 8, 8, 8)
panel:setSize(240, 80)
panel:setPos(0, 0)
hud:add(panel)N-patches can use the same TexturePacker frame table shape as sprites. Border values are in original untrimmed frame pixels; Kaptan remaps them to the trimmed atlas source when setFrame or setBorders changes.
local sheet = dofile('tests/ui/ui-sheet.lua')
local panel = KaptanNPatch.new('tests/ui/' .. sheet.texture)
panel:setFrame(sheet.sprites.panel)
panel:setBorders(8, 8, 8, 8)
panel:setSize(240, 80)Spritesheet n-patches can be stored inside a larger texture because setSourceRect and setFrame select the atlas region. Rotated atlas regions are not supported. Untrimmed n-patch assets are the easiest workflow, but trimmed frame metadata is supported.
This example renders a world sprite affected by the camera and a HUD label that stays fixed near the top of the viewport.
KaptanWindow.open('Kaptan', 1024, 768)
KaptanRenderer.setClearColor(76, 76, 76, 255)
world = KaptanLayer.new()
hud = KaptanLayer.new()
hud:setCamAttached(false)
KaptanRenderer.add(world)
KaptanRenderer.add(hud)
player = KaptanSprite.new('tests/sprites/kaptan1.png')
player:setPos(0, 0)
world:add(player)
label = KaptanText.new('tests/text/unitblock.ttf', 'HP: 100', 32)
viewport_width, viewport_height = KaptanWindow.getViewportSize()
label:setPos(0, -viewport_height / 2 + 40)
hud:add(label)
KaptanCamera.setPos(200, 100)
KaptanCamera.setZoom(2)The player is rendered in world space and moves relative to the camera. The label is rendered in viewport space and remains fixed near the top center of the viewport.
Use KaptanGroup to move, rotate, scale, tint, or hide multiple render items together. A group's transform wraps each child's own transform, so the group can rotate around its pivot while each child still keeps its own pivot.
group = KaptanGroup.new()
group:setPos(0, 0)
group:setPiv(0, 0)
group:setRot(30)
group:setColor(255, 255, 255, 220)
sprite = KaptanSprite.new('tests/sprites/kaptan1.png')
sprite:setPos(-100, 0)
sprite:setRot(-30)
label = KaptanText.new('tests/text/unitblock.ttf', 'Grouped', 32)
label:setPos(100, 0)
group:add(sprite)
group:add(label)
layer:add(group)Groups can contain sprites, N-patches, draw shapes, text, text boxes, and other groups. Nested groups are allowed, but direct or indirect cycles are rejected. The same item may be added to multiple groups or layers. Adding the same item twice to one group returns false.
Groups do not have intrinsic size. Add optional local bounds when you want a group to behave like a button or panel for hit testing. x and y are the local top-left of the bounds rectangle; use negative half-size values for bounds centered on the group pivot.
button = KaptanGroup.new()
button:setBounds(-80, -24, 160, 48)
local x, y, w, h = button:getBounds()
mouse_x, mouse_y = KaptanMouse.getPos()
if button:containsPoint(mouse_x, mouse_y) then
print('button hover')
endgroup:containsPoint(x, y) tests the group's own transform only. For nested UI, call root:containsChildPoint(child_group, x, y) so the hit test includes parent group transforms from that root.
button_row = KaptanGroup.new()
button_row:setPos(300, 100)
play_button = KaptanGroup.new()
play_button:setPos(0, 0)
play_button:setBounds(-80, -24, 160, 48)
button_row:add(play_button)
mouse_x, mouse_y = KaptanMouse.getPos()
if button_row:containsChildPoint(play_button, mouse_x, mouse_y) then
print('play clicked')
endIf a child group is reachable by multiple paths from the root, containsChildPoint returns true if any path contains the point. getBounds() returns local bounds as x, y, width, height, or nil when no bounds are set. clearBounds() removes bounds and makes group hit tests return false until new bounds are set.
Use KaptanTimer for delayed, looping, or ping-pong callbacks. Windowed scripts update timers automatically after physics and before KaptanWindow.setLoopCallback; non-window scripts can use KaptanTimer.step(dt).
local timer = KaptanTimer.new(1.5, function(t)
print('done', t:getTime())
end)
timer:play()KaptanTimer.new(span, callback) is a convenience form. You can also create an inert timer with the default 0 span and configure it later:
local timer = KaptanTimer.new()
timer:setSpan(60)
timer:setCallback(function(t)
print('minute elapsed')
end)
timer:play()Timers support KaptanTimer.ONCE, KaptanTimer.LOOP, and KaptanTimer.PING_PONG. play() runs forward, playReverse() runs backward, and setSpeed(speed) scales elapsed time. Timer callbacks receive the timer as their only argument.
local blink = KaptanTimer.new(0.25, function(t)
cursor:setVisible(not cursor:isVisible())
end)
blink:setLoopMode(KaptanTimer.LOOP)
blink:play()ONCE stops at the reached edge and fires once. LOOP wraps at an edge and fires once per update when wrapping. PING_PONG bounces at an edge, flips direction, and fires once per update when bouncing. If one large update crosses multiple edges, Kaptan fires the callback once for that update.
Timers are discarded when Lua garbage collects their userdata. A discarded timer is removed from the engine timer list and its callback is unreferenced, so it will not fire after GC.
Use KaptanCoroutine for frame-by-frame sequences that are easier to read linearly than as nested callbacks. Windowed scripts resume active coroutines automatically after timers and before KaptanWindow.setLoopCallback; non-window scripts can use KaptanCoroutine.step().
local thread = KaptanCoroutine.new()
thread:run(function()
print('now')
coroutine.yield()
print('next frame')
end)Coroutines can wait on timers, animations, or any Lua predicate:
local function waitSeconds(seconds)
local timer = KaptanTimer.new(seconds)
timer:play()
while timer:isPlaying() do
coroutine.yield()
end
end
local thread = KaptanCoroutine.new()
thread:run(function()
waitSeconds(1)
panel:setVisible(false)
end)thread:run(func, ...) starts immediately and runs until the first coroutine.yield() or completion. Active threads resume once per coroutine update. If Lua garbage collects a coroutine userdata, the coroutine is discarded and will not resume again.
Animations are explicitly updated. Game code controls when animation state advances, usually by calling an animation object's update(dt) from KaptanWindow.setLoopCallback.
Loop modes are shared by animation APIs:
KaptanAnimation.ONCE
KaptanAnimation.LOOP
KaptanAnimation.PING_PONGEasing functions map a normalized time value to an eased value. KaptanEase.sample(ease, t) clamps t to 0..1.
local t = 0.5
local eased = KaptanEase.sample(KaptanEase.OUT_QUAD, t)Scalar curves sample a number from time-based keyframes. Easing belongs to the segment starting at the key where it is set.
local curve = KaptanAnimationCurve.new()
curve:addKey(0.0, 0, KaptanEase.OUT_QUAD)
curve:addKey(1.0, 100)
local value = curve:sample(0.5)Vector curves sample an x, y pair with the same keyframe and easing rules:
local pos_curve = KaptanVec2Curve.new()
pos_curve:addKey(0.0, 0, 0, KaptanEase.OUT_QUAD)
pos_curve:addKey(1.0, 100, 50)
local x, y = pos_curve:sample(0.5)Angle curves sample rotation in degrees. They use shortest-path interpolation by default, so 350 to 10 rotates through 0. Disable shortest-path interpolation when you want raw numeric interpolation.
local rot_curve = KaptanAngleCurve.new()
rot_curve:addKey(0.0, 350)
rot_curve:addKey(1.0, 10)
local angle = rot_curve:sample(0.5)Color curves sample 0..255 RGBA values and return clamped integer channels.
local color_curve = KaptanColorCurve.new()
color_curve:addKey(0.0, 255, 255, 255, 255)
color_curve:addKey(1.0, 255, 0, 0, 128, KaptanEase.OUT_QUAD)
sprite:setColor(color_curve:sample(0.5))Sprite animations play TexturePacker frame tables on a sprite. Call update(dt) each frame while the animation should advance.
local sheet = dofile('tests/spritesheet/sheet.lua')
local sprite = KaptanSprite.new('tests/spritesheet/' .. sheet.texture)
local anim = KaptanSpriteAnimation.new(sprite)
anim:addFrame(sheet.sprites.kaptan1, 1 / 12)
anim:addFrame(sheet.sprites.kaptan2, 1 / 12)
anim:setLoopMode(KaptanAnimation.LOOP)
anim:play()
KaptanWindow.setLoopCallback(function()
anim:update(KaptanWindow.getDeltaTime())
end)Audio initialization is explicit. Call KaptanAudioSystem.init() before loading or playing audio.
KaptanAudioSystem.init()Kaptan does not initialize audio automatically because not every script needs an audio device. This keeps non-audio scripts usable in environments where audio may not be available.
Cleanup is automatic on shutdown if audio was initialized, but you can call KaptanAudioSystem.destroy() manually when entering a mode that no longer needs audio:
KaptanAudioSystem.destroy()Kaptan has two channel kinds:
KaptanAudioChannel.SOUND loads files into memory with Raylib Sound. Use it for short effects like hits, jumps, pickups, UI clicks, and weapon sounds.
KaptanAudioChannel.MUSIC streams from disk with Raylib Music. Use it for longer background music or ambience.
local sfx = KaptanAudioChannel.new(KaptanAudioChannel.SOUND)
local music = KaptanAudioChannel.new(KaptanAudioChannel.MUSIC)Audio channels store named resources. Load a resource once with channel:add(name, path), then play it by name.
local sfx = KaptanAudioChannel.new(KaptanAudioChannel.SOUND)
sfx:add('hit', 'tests/audio/hit.wav')
KaptanAudioSystem.add(sfx)
if KaptanKeyboard.isPressed(KaptanKeyboard.KEY_SPACE) then
sfx:play('hit')
endFor music, create a music channel, load a streamed file, optionally enable looping, register it with the audio system, then play it:
local music = KaptanAudioChannel.new(KaptanAudioChannel.MUSIC)
music:add('theme', 'tests/audio/theme.ogg')
music:setLoop(true)
KaptanAudioSystem.add(music)
music:play('theme')channel:pause(), channel:resume(), channel:stop(), and channel:isPlaying() operate on the channel's active resource. The active resource is set by the most recent channel:play(name) call.
KaptanAudioSystem.add(channel) registers a channel with the audio system. The audio system does not accept duplicate references to the same channel. KaptanAudioSystem.add(channel) returns true when it registers a new channel and false when the channel is already registered.
Registered channels are kept alive by the system and released by KaptanAudioSystem.remove(channel), KaptanAudioSystem.clear(), or KaptanAudioSystem.destroy(). Removing a channel releases the system's reference without destroying a Lua-owned channel. KaptanAudioSystem.remove(channel) returns true when it removes a channel and false when the channel was not registered.
Music channels registered with the audio system are updated automatically once per frame. This is required by Raylib streamed music playback.
KaptanAudioSystem.add(music)If you do not add a music channel to the audio system, its stream will not be updated automatically.
Volume, pan, and pitch are channel-wide settings. They are applied to loaded resources and reused when a named resource is played.
sfx:setVolume(0.75)
sfx:setPan(0.5)
sfx:setPitch(1.0)channel:setLoop(loop) is only valid for music channels. Calling it on a sound channel raises a Lua error.
music:setLoop(true)Load audio once, then play by name. Avoid loading files every time an effect plays.
Use a few sound channels for categories such as combat, UI, and ambience. Use music channels for streamed tracks.
Register music channels with KaptanAudioSystem.add(channel) so streaming updates happen automatically.
Use KaptanAudioSystem.remove(channel) when a channel should stop being owned and updated by the audio system.
Use KaptanAudioSystem.clear() to release all registered channels when changing scenes or resetting audio state:
KaptanAudioSystem.clear()Physics initialization is explicit. Call KaptanPhysics.init() before creating or updating physics objects.
KaptanPhysics.init()Physics uses the same world coordinate model as rendering. Positive x moves right, positive y moves down, and {0, 0} is the world center. A physics body position can be copied directly to a sprite on a camera-attached layer.
Physics is updated automatically by Kaptan while the window loop is running. It uses a fixed timestep with a default tick rate of 60 Hz, so physics stays stable when rendering frame time varies.
Use KaptanPhysics.step(dt) only for scripts that do not open a window, such as offline simulations or command-line tools. Do not call it inside KaptanWindow.setLoopCallback, because the window loop already steps physics automatically.
Use KaptanPhysics.pause() and KaptanPhysics.resume() for game pause flows. Paused physics skips both automatic window-loop updates and manual KaptanPhysics.step(dt) calls; queries, debug draw, and event polling remain available.
KaptanPhysics.setGravity(0, 0)
KaptanPhysics.setSubsteps(2)
KaptanPhysics.setTickRate(60)KaptanPhysics.setUnitsPerMeter(value) configures Box2D's length scale. It must be called before KaptanPhysics.init(). The default is 64 Kaptan units per meter.
Use KaptanPhysics.clear() to reset the world while keeping physics initialized. Use KaptanPhysics.destroy() when physics is no longer needed.
Create bodies with KaptanPhysicsBody.new(kind). Body kinds are KaptanPhysicsBody.STATIC, KaptanPhysicsBody.KINEMATIC, and KaptanPhysicsBody.DYNAMIC.
local enemy_body = KaptanPhysicsBody.new(KaptanPhysicsBody.DYNAMIC)
print(enemy_body:isValid())body:destroy() explicitly removes the body from the physics world. KaptanPhysics.clear() and KaptanPhysics.destroy() invalidate all existing body handles, so body:isValid() returns false after the world is cleared or destroyed.
Body positions use Kaptan world coordinates. Rotation is exposed in degrees, matching sprites, text, draw shapes, and the camera. Linear velocity uses Kaptan units per second.
Use forces and impulses to move dynamic bodies through the physics simulation. applyForce and applyImpulse act at the body's center, while applyTorque and applyAngularImpulse rotate it.
enemy_body:setPos(0, 0)
enemy_body:setVelocity(120, 0)
enemy_body:applyImpulse(300, 0)
enemy_body:setFixedRotation(true)
local x, y = enemy_body:getPos()
enemy_sprite:setPos(x, y)Add shapes to bodies to make them collide. Shape creation supports circles, boxes, rounded boxes, capsules, and convex polygons.
local enemy_shape = enemy_body:addCircle(16)
local wall_shape = wall_body:addBox(200, 32)
local rounded_wall_shape = wall_body:addBox(200, 32, 6)
local capsule_shape = enemy_body:addCapsule(24, 48, 12)
local triangle_shape = enemy_body:addPolygon({ -12, -8, 12, -8, 0, 14 })Polygons must be convex and use at most 8 points. addPolygon accepts a flat { x1, y1, x2, y2, ... } table in body-local coordinates.
Pass an options table when creating a shape to make it a sensor or enable event collection:
local pickup_sensor = pickup_body:addCircle(24, {
sensor = true,
sensorEvents = true,
})
local player_shape = player_body:addCircle(16, {
sensorEvents = true,
})Sensors are creation-time behavior in Box2D. Use shape:isSensor() to inspect a shape, and create a new shape if you need to switch between solid and sensor behavior.
Poll contact and sensor events after physics updates:
- During the automatic window loop, Kaptan keeps the latest update's events, including all fixed physics ticks that ran during that update.
- Unpolled automatic events are discarded when the next automatic physics update begins, so event buffers stay bounded.
- Manual
KaptanPhysics.step(dt)calls accumulate events until you callgetContactEvents()orgetSensorEvents(). - Contact events require
contactEvents = trueon at least one participating shape. - Sensor events require
sensorEvents = trueon the sensor shape and on the overlapping visitor shape. The visitor can still be solid; it does not needsensor = true.
for _, event in ipairs(KaptanPhysics.getContactEvents()) do
if event.kind == "begin" then
print("contact", event.shapeA, event.shapeB)
end
end
for _, event in ipairs(KaptanPhysics.getSensorEvents()) do
if event.kind == "begin" then
print("sensor", event.sensor, event.visitor)
end
endEvent shape fields can be nil for end events if Box2D reports a shape that was already destroyed. Check fields before using them.
Use queries to find shapes without changing the simulation:
local hits = KaptanPhysics.queryAABB(0, 0, 128, 128, {
mask = CATEGORY_ENEMY,
})
local hit = KaptanPhysics.raycast(0, 0, 400, 0, {
mask = CATEGORY_WALL,
})
if hit then
print(hit.shape, hit.x, hit.y, hit.normalX, hit.normalY, hit.fraction)
endqueryAABB(x, y, width, height, options) treats x, y as the center of the query box. Query options support category and mask bits. raycast returns the closest hit or nil.
Use tags to attach game-defined labels to bodies and shapes. Tags have no engine-defined meaning; they are just strings you can read from event/query results.
enemy_body:setTag("enemy")
enemy_shape:setTag("enemy_hitbox")
local hit = KaptanPhysics.raycast(0, 0, 400, 0)
if hit and hit.shape:getTag() == "enemy_hitbox" then
print("hit enemy")
endEnable debug drawing in debug builds to render physics shapes as a world-space overlay after normal layers. It uses the main camera and does not require a KaptanLayer.
KaptanPhysics.setDebugDraw(true)Debug draw is intended for development diagnostics, not game visuals. It only appears while a window is open.
Use category and mask bits to control which shapes collide or appear in queries:
local bit = require('bit')
local CATEGORY_PLAYER = bit.lshift(1, 0)
local CATEGORY_ENEMY = bit.lshift(1, 1)
local CATEGORY_WALL = bit.lshift(1, 2)
enemy_shape:setCategory(CATEGORY_ENEMY)
enemy_shape:setMask(bit.bor(CATEGORY_PLAYER, CATEGORY_WALL))Kaptan does not define game-specific category constants. Define category bits in Lua for each game.
shape:destroy() removes one shape from its body. body:destroy(), KaptanPhysics.clear(), and KaptanPhysics.destroy() invalidate all shapes attached to destroyed bodies.
Use KaptanSpatial for lightweight gameplay queries that do not need Box2D collision simulation. Spatial items are point, circle, rect, or ellipse markers stored in Kaptan world coordinates. Spatial queries are intended for modest entity counts and gameplay checks such as proximity, targeting, pickups, and UI-free trigger zones.
local space = KaptanSpatial.new()
local enemy = space:addCircle(100, 0, 24)
enemy:setTag('enemy')
local pickup = space:addRect(-50, 0, 32, 32)
pickup:setTag('pickup')
pickup:setEnabled(false)
for _, item in ipairs(space:queryCircle(0, 0, 160)) do
if item:getTag() == 'enemy' then
print('near enemy')
end
end
local closest = space:nearest(0, 0, 500)
if closest then
print(closest.item:getTag(), closest.distance)
end
local closestItem = space:nearestItem(0, 0, 500)
if closestItem then
print(closestItem:getTag())
end
local hits = {}
local count = space:queryCircleInto(hits, 0, 0, 160)
for i = 1, count do
print(hits[i]:getTag())
end
if space:anyCircle(0, 0, 160) then
print('something nearby')
end
space:setEnabled(false) -- skip all spatial query work (e.g. while the game is paused)addPoint(x, y), addCircle(x, y, radius), addRect(x, y, width, height), and addEllipse(x, y, radiusX, radiusY) return KaptanSpatialItem handles. Items can be moved, reshaped, tagged, disabled, or removed:
item:setPos(x, y)moves an item.item:setCircle,item:setRect, anditem:setEllipsechange its shape.item:setTag(tag)stores a game-defined label.item:setEnabled(false)keeps the item valid but excludes it from queries.space:remove(item)removes one item.space:setEnabled(false)makes all query and nearest APIs return immediately, useful for pause, death, or cutscene states.space:clear()invalidates all items in the space.
Query behavior:
queryAABB,queryCircle, andqueryEllipsereturn arrays of spatial item handles.nearest(x, y, maxDistance)returns{ item, x, y, distance }ornil.nearestItem(x, y, maxDistance)returns only the closest item ornil.maxDistanceis optional.- For per-frame checks, prefer
anyAABB,anyCircle,anyEllipse,countAABB,countCircle, orcountEllipsebecause they avoid result tables and item handles. queryAABBInto,queryCircleInto,queryEllipseInto, andnearestIntoreuse a caller-provided table and clear stale entries.
Kaptan exposes input through singleton globals. Input is polled from Lua, usually inside KaptanWindow.setLoopCallback.
Supported devices are keyboard, mouse, and gamepad.
Keyboard constants are exposed as KaptanKeyboard.KEY_*.
KaptanKeyboard.isDown(key) is true while a key is held. KaptanKeyboard.isPressed(key) is true only on the frame where the key transitions from up to down. KaptanKeyboard.isReleased(key) is true only on the release frame. KaptanKeyboard.isUp(key) is true while the key is not held.
KaptanKeyboard.getKeysDown() returns an array of currently held key codes.
KaptanKeyboard.getTextInput() returns the complete UTF-8 text typed since the last text-input poll. It consumes Raylib's queued text input, so call it once per frame. Use key APIs such as KaptanKeyboard.isPressed(KaptanKeyboard.KEY_BACKSPACE) for editing keys that are not text characters.
KaptanWindow.setLoopCallback(function()
if KaptanKeyboard.isPressed(KaptanKeyboard.KEY_SPACE) then
print('jump')
end
if KaptanKeyboard.isDown(KaptanKeyboard.KEY_Q) then
KaptanWindow.quit()
end
end)local text = ''
KaptanWindow.setLoopCallback(function()
text = text .. KaptanKeyboard.getTextInput()
if KaptanKeyboard.isPressed(KaptanKeyboard.KEY_BACKSPACE) then
text = text:sub(1, -2)
end
end)Mouse button constants are exposed as KaptanMouse.BUTTON_*.
KaptanMouse.getScreenPos() returns raw top-left window coordinates. This matches Raylib screen coordinates and is not remapped to the viewport.
KaptanMouse.getPos() returns center-relative viewport coordinates. Use it for GUI or HUD elements on layers with layer:setCamAttached(false).
KaptanMouse.getWorldPos() converts the current mouse position through KaptanCamera after remapping from the stretched window to the viewport. Use it for picking or placing objects in camera-attached world layers.
KaptanMouse.getDelta() returns mouse movement since the last frame. KaptanMouse.getWheel() returns vertical wheel movement. KaptanMouse.getWheelV() returns horizontal and vertical wheel movement.
Choose mouse coordinates based on the thing you are interacting with:
- GUI or HUD on a detached layer:
KaptanMouse.getPos(). - World objects on a camera-attached layer:
KaptanMouse.getWorldPos(). - Raw top-left window coordinates, bypassing viewport scaling:
KaptanMouse.getScreenPos().
if KaptanMouse.isPressed(KaptanMouse.BUTTON_LEFT) then
local x, y = KaptanMouse.getWorldPos()
print('clicked world position', x, y)
endUse sprite:containsPoint(x, y) for rectangle-based sprite picking. Pass KaptanMouse.getPos() for detached GUI layers and KaptanMouse.getWorldPos() for camera-attached world layers. Invisible sprites return false.
if KaptanMouse.isPressed(KaptanMouse.BUTTON_LEFT) then
local x, y = KaptanMouse.getPos()
if button:containsPoint(x, y) then
print('clicked button')
end
endFor UI built from nested groups, set bounds on the button group and call root:containsChildPoint(button, x, y) so parent group transforms are included.
local x, y = KaptanMouse.getPos()
cursor_label:setPos(x, y)Gamepad indices are Lua-style: 1 is the first connected gamepad. Internally, Kaptan converts this to Raylib's zero-based gamepad index.
PS and Xbox controllers use Raylib's generic gamepad layout. Button constants are exposed as KaptanGamepad.BUTTON_*. Axis constants are exposed as KaptanGamepad.AXIS_*.
Use KaptanGamepad.getLeftStick(gamepad), KaptanGamepad.getRightStick(gamepad), and KaptanGamepad.getTriggers(gamepad) for common controller input. Use KaptanGamepad.getAxis(gamepad, axis) for raw axis access.
Apply deadzones to analog stick input to avoid drift.
local pad = 1
KaptanWindow.setLoopCallback(function()
if KaptanGamepad.isAvailable(pad) then
local x, y = KaptanGamepad.getLeftStick(pad)
if math.abs(x) > 0.2 or math.abs(y) > 0.2 then
print('move', x, y)
end
if KaptanGamepad.isPressed(pad, KaptanGamepad.BUTTON_RIGHT_FACE_DOWN) then
print('confirm')
end
end
end)Use isPressed for one-shot actions like jump, confirm, pause, or opening a menu.
Use isDown for continuous actions like movement, charging, aiming, or holding a button.
Use KaptanMouse.getWorldPos() when interacting with world objects.
Use KaptanMouse.getPos() when interacting with GUI or HUD objects.
Use a deadzone for analog sticks to avoid drift.
Use KaptanJSON.decode(json_string) to parse JSON text into Lua tables and KaptanJSON.encode(table) to serialize Lua tables back to JSON text.
local data = KaptanJSON.decode([[{"name":"Kaptan","enabled":true,"tags":["engine","lua"]}]])
print(data.name, data.enabled, data.tags[1])
data.version = 1
local json = KaptanJSON.encode(data)
print(json)Decoded JSON objects become Lua tables with string keys. Decoded arrays become 1-based Lua arrays. JSON numbers become Lua numbers, booleans become Lua booleans, strings become Lua strings, and null becomes nil.
Encoding expects a Lua table. Tables with array entries are encoded as JSON arrays; other string-keyed tables are encoded as JSON objects. Non-string keys in object-style tables are skipped.
Kaptan objects are split between Lua handles and Odin-owned runtime objects. In normal game code, the practical rules are:
- Keep Lua variables for objects you plan to update later.
- Use
removeorclearwhen something must disappear immediately. - Setting a Lua variable to
nilonly drops Lua's handle; final cleanup happens when Lua GC runs and engine references are released.
Adding an item to a layer or group gives that container a reference. Removing the item releases the reference without destroying your Lua-owned object.
local sprite = KaptanSprite.new('tests/sprites/kaptan1.png')
world:add(sprite)
world:remove(sprite) -- stops rendering it from this layerIf Lua drops the last handle to an object while a layer, group, renderer, or audio system still references it, Kaptan marks it for removal and frees it later when the engine reference is released. This avoids use-after-free while still letting discarded Lua handles disappear from render and update lists.
Use layer:clear() or group:clear() when a scene, particle group, or HUD layer needs to drop every item it owns. Use KaptanRenderer.clear() when replacing the whole render graph.
Sprites use a texture cache. Text and text boxes use a font cache. Loading the same texture path or the same font path and font size reuses the cached resource, and cached resources unload when their reference count reaches zero.
Avoid creating and discarding large numbers of objects every frame. For high-frequency effects such as damage numbers, prefer pooling reusable text objects:
local damage = KaptanText.new('tests/text/unitblock.ttf', '', 28)
damage:setVisible(false)
hud:add(damage)
function show_damage(amount, x, y)
damage:setText(tostring(amount))
damage:setPos(x, y)
damage:setVisible(true)
endKaptan userdata can also store Lua-side fields and methods. object:setInterface(interface_table) attaches a Lua interface table while keeping native Kaptan methods available as a fallback.
local interface = {}
interface.__index = interface
function interface:init(value)
self.value = value
end
function interface:half()
return self:sample(0.5)
end
local curve = KaptanAnimationCurve.new()
curve:setInterface(interface)
curve:init(20)
print(curve.value) -- Lua-side field
print(curve:half()) -- Lua-side method
print(curve:getKeyCount()) -- native Kaptan fallbackUse KaptanEnvironment.setLuaGCLogging(true) when diagnosing Lua ownership or collection behavior. Use KaptanEnvironment.setFrameProfiling(true) when tracking frame cost during development.
This is a compact API reference. The sections above show typical usage.
All Kaptan userdata objects support object:setInterface(interface_table). This includes layers, groups, sprites, n-patches, draw shapes, text, text boxes, audio channels, animation curves, sprite animations, physics bodies, physics shapes, spatial spaces, and spatial items.
Groups, sprites, N-patches, text, text boxes, and draw shapes share transform, color, and visibility methods: getPos, setPos, getPiv, setPiv, getRot, setRot, getScl, setScl, getColor, setColor, isVisible, and setVisible.
- KaptanEnvironment.getFrameProfile()
- KaptanEnvironment.isDebugBuild()
- KaptanEnvironment.isFrameProfiling()
- KaptanEnvironment.isFPSCounterEnabled()
- KaptanEnvironment.isLuaGCLogging()
- KaptanEnvironment.resetFrameProfile()
- KaptanEnvironment.setFrameProfiling(enabled)
- KaptanEnvironment.setFPSCounterEnabled(enabled)
- KaptanEnvironment.setLuaGCLogging(enabled)
- KaptanWindow.open(title, width, height)
- KaptanWindow.clearLoopCallback()
- KaptanWindow.getDeltaTime()
- KaptanWindow.getFPS()
- KaptanWindow.getHeight()
- KaptanWindow.getViewportSize()
- KaptanWindow.getWidth()
- KaptanWindow.setFullscreen(enabled)
- KaptanWindow.setLoopCallback(func)
- KaptanWindow.setMaxFPS(fps)
- KaptanWindow.setViewportSize(width, height)
- KaptanWindow.setVsync(enabled)
- KaptanWindow.showMouseCursor(visible)
- KaptanWindow.quit()
- KaptanRenderer.add(layer)
- KaptanRenderer.clear()
- KaptanRenderer.remove(layer)
- KaptanRenderer.setClearColor(r, g, b, a)
- KaptanCamera.getPiv()
- KaptanCamera.getPos()
- KaptanCamera.getRot()
- KaptanCamera.getZoom()
- KaptanCamera.setPiv(x, y)
- KaptanCamera.setPos(x, y)
- KaptanCamera.setRot(angle)
- KaptanCamera.setZoom(zoom)
- layer = KaptanLayer.new()
- layer:add(sprite_or_shape_or_text_or_npatch_or_group)
- layer:clear()
- layer:isCamAttached()
- layer:isVisible()
- layer:remove(sprite_or_shape_or_text_or_npatch_or_group)
- layer:setCamAttached(attached)
- layer:setVisible(visible)
- group = KaptanGroup.new()
- group:add(sprite_or_shape_or_text_or_npatch_or_group)
- group:clear()
- group:clearBounds()
- group:containsChildPoint(child_group, x, y)
- group:containsPoint(x, y)
- group:getBounds()
- group:getColor()
- group:getPiv()
- group:getPos()
- group:getRot()
- group:getScl()
- group:isVisible()
- group:remove(sprite_or_shape_or_text_or_npatch_or_group)
- group:setBounds(x, y, width, height)
- group:setColor(r, g, b, a)
- group:setPiv(x, y)
- group:setPos(x, y)
- group:setRot(angle)
- group:setScl(x, y)
- group:setVisible(visible)
- sprite = KaptanSprite.new(path)
- sprite:containsPoint(x, y)
- sprite:getColor()
- sprite:getPiv()
- sprite:getPos()
- sprite:getRot()
- sprite:getScl()
- sprite:getSize()
- sprite:isVisible()
- sprite:setColor(r, g, b, a)
- sprite:setFrame(frame_table)
- sprite:setFrameSize(w, h)
- sprite:setOffset(x, y)
- sprite:setPiv(x, y)
- sprite:setPos(x, y)
- sprite:setRot(angle)
- sprite:setScl(x, y)
- sprite:setSourceRect(x, y, w, h)
- sprite:setVisible(visible)
- patch = KaptanNPatch.new(path)
- KaptanNPatch.LAYOUT_9PATCH
- KaptanNPatch.LAYOUT_3PATCH_VERTICAL
- KaptanNPatch.LAYOUT_3PATCH_HORIZONTAL
- patch:containsPoint(x, y)
- patch:getColor()
- patch:getPiv()
- patch:getPos()
- patch:getRot()
- patch:getScl()
- patch:getSize()
- patch:isVisible()
- patch:setBorders(left, top, right, bottom)
- patch:setColor(r, g, b, a)
- patch:setFrame(frame_table)
- patch:setLayout(layout)
- patch:setPiv(x, y)
- patch:setPos(x, y)
- patch:setRot(angle)
- patch:setScl(x, y)
- patch:setSize(width, height)
- patch:setSourceRect(x, y, w, h)
- patch:setVisible(visible)
- text = KaptanText.new(font_path, text, font_size)
- text:getColor()
- text:getPiv()
- text:getPos()
- text:getRot()
- text:getScl()
- text:getSize()
- text:isVisible()
- text:setColor(r, g, b, a)
- text:setPiv(x, y)
- text:setPos(x, y)
- text:setRot(angle)
- text:setScl(scale_x, scale_y)
- text:setText(text)
- text:setVisible(visible)
- text_box = KaptanTextBox.new(font_path, text, font_size, width, height)
- KaptanTextBox.ALIGN_LEFT
- KaptanTextBox.ALIGN_CENTER
- KaptanTextBox.ALIGN_RIGHT
- text_box:getAlignment()
- text_box:getColor()
- text_box:getPiv()
- text_box:getPos()
- text_box:getRot()
- text_box:getScl()
- text_box:getSize()
- text_box:isVisible()
- text_box:setAlignment(alignment)
- text_box:setColor(r, g, b, a)
- text_box:setPiv(x, y)
- text_box:setPos(x, y)
- text_box:setRot(angle)
- text_box:setScl(scale_x, scale_y)
- text_box:setSize(width, height)
- text_box:setText(text)
- text_box:setVisible(visible)
- shape = KaptanDraw.newPoint(x, y)
- shape = KaptanDraw.newLine(x1, y1, x2, y2)
- shape = KaptanDraw.newRect(x, y, width, height)
- shape = KaptanDraw.newCircle(x, y, radius)
- shape = KaptanDraw.newEllipse(x, y, radiusX, radiusY)
- shape = KaptanDraw.newPolygon(points) -- a flat list:
{x1, y1, x2, y2, ...} - shape:getColor()
- shape:getPiv()
- shape:getPos()
- shape:getRot()
- shape:getScl()
- shape:isVisible()
- shape:setBorderColor(r, g, b, a)
- shape:setBorderSize(size)
- shape:setColor(r, g, b, a)
- shape:setPiv(x, y)
- shape:setPos(x, y)
- shape:setRot(angle)
- shape:setScl(x, y)
- shape:setVisible(visible)
- KaptanAnimation.ONCE
- KaptanAnimation.LOOP
- KaptanAnimation.PING_PONG
- KaptanEase.sample(ease, t)
- KaptanEase.LINEAR
- KaptanEase.STEP
- KaptanEase.IN_QUAD
- KaptanEase.OUT_QUAD
- KaptanEase.IN_OUT_QUAD
- KaptanEase.IN_CUBIC
- KaptanEase.OUT_CUBIC
- KaptanEase.IN_OUT_CUBIC
- KaptanEase.IN_SINE
- KaptanEase.OUT_SINE
- KaptanEase.IN_OUT_SINE
- KaptanEase.IN_BACK
- KaptanEase.OUT_BACK
- KaptanEase.IN_OUT_BACK
- curve = KaptanAnimationCurve.new()
- curve:addKey(time, value, ease)
- curve:clear()
- curve:getDuration()
- curve:getKeyCount()
- curve:removeKey(index)
- curve:sample(time)
- curve = KaptanVec2Curve.new()
- curve:addKey(time, x, y, ease)
- curve:clear()
- curve:getDuration()
- curve:getKeyCount()
- curve:removeKey(index)
- curve:sample(time)
- curve = KaptanAngleCurve.new()
- curve:addKey(time, angle, ease)
- curve:clear()
- curve:getDuration()
- curve:getKeyCount()
- curve:isShortestPath()
- curve:removeKey(index)
- curve:sample(time)
- curve:setShortestPath(enabled)
- curve = KaptanColorCurve.new()
- curve:addKey(time, r, g, b, a, ease)
- curve:clear()
- curve:getDuration()
- curve:getKeyCount()
- curve:removeKey(index)
- curve:sample(time)
- anim = KaptanSpriteAnimation.new(sprite)
- anim:addFrame(frame_table, duration)
- anim:clear()
- anim:getDuration()
- anim:getFrameIndex()
- anim:getLoopMode()
- anim:getSpeed()
- anim:getTime()
- anim:isFinished()
- anim:isPlaying()
- anim:pause()
- anim:play()
- anim:restart()
- anim:seek(time)
- anim:setFrameIndex(index)
- anim:setLoopMode(mode)
- anim:setSpeed(speed)
- anim:stop()
- anim:update(dt)
- timer = KaptanTimer.new(span_or_nil, callback_or_nil)
- KaptanTimer.ONCE
- KaptanTimer.LOOP
- KaptanTimer.PING_PONG
- KaptanTimer.clear()
- KaptanTimer.step(dt)
- timer:getDirection()
- timer:getLoopMode()
- timer:getProgress()
- timer:getSpan()
- timer:getSpeed()
- timer:getTime()
- timer:isFinished()
- timer:isPlaying()
- timer:isValid()
- timer:pause()
- timer:play()
- timer:playReverse()
- timer:restart()
- timer:seek(time)
- timer:setCallback(callback_or_nil)
- timer:setDirection(direction)
- timer:setLoopMode(mode)
- timer:setSpan(span)
- timer:setSpeed(speed)
- timer:stop()
- timer:update(dt)
- thread = KaptanCoroutine.new()
- KaptanCoroutine.clear()
- KaptanCoroutine.step()
- thread:isFinished()
- thread:isRunning()
- thread:isValid()
- thread:run(func, ...)
- thread:stop()
- KaptanAudioSystem.init()
- KaptanAudioSystem.destroy()
- KaptanAudioSystem.isReady()
- KaptanAudioSystem.add(channel)
- KaptanAudioSystem.clear()
- KaptanAudioSystem.remove(channel)
- KaptanAudioSystem.setMasterVolume(volume)
- KaptanAudioSystem.getMasterVolume()
- channel = KaptanAudioChannel.new(kind)
- channel:add(name, path)
- channel:clear()
- channel:play(name)
- channel:pause()
- channel:resume()
- channel:stop()
- channel:isPlaying()
- channel:setVolume(volume)
- channel:setPan(pan)
- channel:setPitch(pitch)
- channel:setLoop(loop)
- KaptanAudioChannel.SOUND
- KaptanAudioChannel.MUSIC
- KaptanPhysics.clear()
- KaptanPhysics.destroy()
- KaptanPhysics.getContactEvents()
- KaptanPhysics.getGravity()
- KaptanPhysics.getSensorEvents()
- KaptanPhysics.getSubsteps()
- KaptanPhysics.getTickRate()
- KaptanPhysics.getUnitsPerMeter()
- KaptanPhysics.init()
- KaptanPhysics.isDebugDraw()
- KaptanPhysics.isPaused()
- KaptanPhysics.isReady()
- KaptanPhysics.pause()
- KaptanPhysics.queryAABB(x, y, width, height, options)
- KaptanPhysics.raycast(x1, y1, x2, y2, options)
- KaptanPhysics.resume()
- KaptanPhysics.setDebugDraw(enabled)
- KaptanPhysics.setGravity(x, y)
- KaptanPhysics.setSubsteps(count)
- KaptanPhysics.setTickRate(hz)
- KaptanPhysics.setUnitsPerMeter(value)
- KaptanPhysics.step(dt) -- non-window/manual scripts only
- body = KaptanPhysicsBody.new(kind)
- body:addBox(width, height, options)
- body:addBox(width, height, radius, options)
- body:addCapsule(width, height, radius, options)
- body:addCircle(radius, options)
- body:addPolygon(points, options)
- body:applyAngularImpulse(value)
- body:applyForce(x, y)
- body:applyImpulse(x, y)
- body:applyTorque(value)
- body:destroy()
- body:getAngularDamping()
- body:getAngularVelocity()
- body:getId()
- body:getLinearDamping()
- body:getPos()
- body:getRot()
- body:getTag()
- body:getType()
- body:getVelocity()
- body:isBullet()
- body:isEnabled()
- body:isFixedRotation()
- body:isValid()
- body:setAngularDamping(value)
- body:setAngularVelocity(value)
- body:setBullet(enabled)
- body:setEnabled(enabled)
- body:setFixedRotation(enabled)
- body:setLinearDamping(value)
- body:setPos(x, y)
- body:setRot(angle)
- body:setTag(tag)
- body:setType(kind)
- body:setVelocity(x, y)
- KaptanPhysicsBody.STATIC
- KaptanPhysicsBody.KINEMATIC
- KaptanPhysicsBody.DYNAMIC
- shape:destroy()
- shape:getCategory()
- shape:getDensity()
- shape:getFriction()
- shape:getGroup()
- shape:getId()
- shape:getMask()
- shape:getRestitution()
- shape:getTag()
- shape:isContactEvents()
- shape:isHitEvents()
- shape:isSensor()
- shape:isSensorEvents()
- shape:isValid()
- shape:setCategory(bits)
- shape:setContactEvents(enabled)
- shape:setDensity(value)
- shape:setFriction(value)
- shape:setGroup(group)
- shape:setHitEvents(enabled)
- shape:setMask(bits)
- shape:setRestitution(value)
- shape:setSensorEvents(enabled)
- shape:setTag(tag)
- space = KaptanSpatial.new()
- item = space:addCircle(x, y, radius)
- item = space:addEllipse(x, y, radiusX, radiusY)
- item = space:addPoint(x, y)
- item = space:addRect(x, y, width, height)
- space:anyAABB(x, y, width, height)
- space:anyCircle(x, y, radius)
- space:anyEllipse(x, y, radiusX, radiusY)
- space:clear()
- space:countAABB(x, y, width, height)
- space:countCircle(x, y, radius)
- space:countEllipse(x, y, radiusX, radiusY)
- space:isEnabled()
- space:nearest(x, y, maxDistance)
- space:nearestInto(resultTable, x, y, maxDistance)
- space:nearestItem(x, y, maxDistance)
- space:queryAABB(x, y, width, height)
- space:queryAABBInto(resultTable, x, y, width, height)
- space:queryCircle(x, y, radius)
- space:queryCircleInto(resultTable, x, y, radius)
- space:queryEllipse(x, y, radiusX, radiusY)
- space:queryEllipseInto(resultTable, x, y, radiusX, radiusY)
- space:remove(item)
- space:setEnabled(enabled)
- item:getPos()
- item:getTag()
- item:isEnabled()
- item:isValid()
- item:setCircle(radius)
- item:setEnabled(enabled)
- item:setEllipse(radiusX, radiusY)
- item:setPos(x, y)
- item:setRect(width, height)
- item:setTag(tag)
- KaptanKeyboard.getKeysDown()
- KaptanKeyboard.getTextInput()
- KaptanKeyboard.isDown(key)
- KaptanKeyboard.isPressed(key)
- KaptanKeyboard.isReleased(key)
- KaptanKeyboard.isUp(key)
- KaptanKeyboard.KEY_*
- KaptanMouse.getDelta()
- KaptanMouse.getPos()
- KaptanMouse.getScreenPos()
- KaptanMouse.getWheel()
- KaptanMouse.getWheelV()
- KaptanMouse.getWorldPos()
- KaptanMouse.isDown(button)
- KaptanMouse.isPressed(button)
- KaptanMouse.isReleased(button)
- KaptanMouse.isUp(button)
- KaptanMouse.BUTTON_*
- KaptanGamepad.getAxis(gamepad, axis)
- KaptanGamepad.getAxisCount(gamepad)
- KaptanGamepad.getButtonPressed()
- KaptanGamepad.getLeftStick(gamepad)
- KaptanGamepad.getName(gamepad)
- KaptanGamepad.getRightStick(gamepad)
- KaptanGamepad.getTriggers(gamepad)
- KaptanGamepad.isAvailable(gamepad)
- KaptanGamepad.isDown(gamepad, button)
- KaptanGamepad.isPressed(gamepad, button)
- KaptanGamepad.isReleased(gamepad, button)
- KaptanGamepad.isUp(gamepad, button)
- KaptanGamepad.AXIS_*
- KaptanGamepad.BUTTON_*
- table = KaptanJSON.decode(json_string)
- json_string = KaptanJSON.encode(table)