Shooting range
A wall of forty-five crates and a barrel to knock it down with. Click to take the mouse, aim with it, fire with the left button, R rebuilds the wall.
local natiny = require("natiny")
natiny.backend.init(arg and arg[1] or natiny.backend.AUTO)
natiny.physics.init()
local win = natiny.window.create("Natiny - Shooting range", 960, 640)
natiny.window.set_scale_mode(win, natiny.window.SCALE_MODE_STRETCH)
local BG = natiny.color.from_hex("#151E2C")
-- Metres, kilograms, seconds: the units the solver works in. The crate and
-- the barrel are both shipped at twice the size this scene wants, so both are
-- worn at half - the collider is made at the size the thing really is and the
-- model is scaled onto it, because scaling an entity never scales a collider.
local SCALE = 0.5
local BOX = 0.5 * SCALE
local BARREL = { radius = 0.267 * SCALE, height = 0.858 * SCALE }
local ROWS = 9 -- 9 + 8 + ... + 1 crates, which is 45 of them
-- The camera never leaves this spot; the mouse only turns it.
local CAMERA = { x = 0, y = 1.2, z = 3.8, fov = 55, sensitivity = 0.12 }
local SHOT = { speed = 12, density = 800, spawn = 0.8, limit = 12 }
local GONE = -2.0 -- under the platform, and still falling
local box_mesh, barrel_mesh
local crates, barrels = {}, {}
local yaw, pitch = 0, -6
local camera = natiny.camera.create()
natiny.camera.set_fov(camera, CAMERA.fov)
natiny.camera.set_clip(camera, 0.1, 100.0)
natiny.entity.set_position(camera, natiny.entity.SPACE_WORLD, CAMERA.x, CAMERA.y, CAMERA.z)
natiny.entity.set_rotation(camera, natiny.entity.SPACE_WORLD, pitch, yaw, 0)
-- Nine rows, one crate shorter each time. Every crate is a cube body with
-- the model hung off it, so the wall is only its own weight and friction.
local function build_stack()
for row = 0, ROWS - 1 do
local count = ROWS - row
for i = 1, count do
local body = natiny.physics.cube(BOX, BOX, BOX)
natiny.entity.set_position(body, natiny.entity.SPACE_WORLD,
(i - (count + 1) / 2) * (BOX + 0.01),
BOX / 2 + row * (BOX + 0.005),
0)
natiny.physics.set_material(body, 0.6, 0.0, 400)
natiny.physics.set_interpolation(body, true)
local model = natiny.model.create(box_mesh)
natiny.entity.set_scale(model, SCALE, SCALE, SCALE)
natiny.entity.set_parent(model, body)
crates[#crates + 1] = { body = body, model = model }
end
end
end
-- A body and the model on it are two objects, and both have to go; a crate
-- that fell off the platform is still falling and would go on falling.
local function clear(items)
for _, item in ipairs(items) do
natiny.model.destroy(item.model)
natiny.physics.destroy(item.body)
end
end
local function reset()
clear(crates)
clear(barrels)
crates, barrels = {}, {}
build_stack()
end
-- Which barrel to give up when the limit is reached: the first one that went
-- over the edge, and only if none did, the oldest. Oldest alone would take
-- the barrel lying in plain sight on the platform and leave a dozen falling
-- through nothing, because a barrel that missed keeps its place in the queue.
local function spare()
for i, barrel in ipairs(barrels) do
local _, y = natiny.entity.get_position(barrel.body, natiny.entity.SPACE_WORLD)
if y < GONE then return i end
end
return 1
end
-- A barrel leaves the camera at the same speed whatever it weighs, which is
-- what set_velocity is for; an impulse would depend on its mass.
local function shoot(dx, dy, dz)
local body = natiny.physics.cylinder(BARREL.radius, BARREL.height)
local model = natiny.model.create(barrel_mesh)
natiny.entity.set_scale(model, SCALE, SCALE, SCALE)
natiny.entity.set_position(body, natiny.entity.SPACE_WORLD,
CAMERA.x + dx * SHOT.spawn,
CAMERA.y + dy * SHOT.spawn,
CAMERA.z + dz * SHOT.spawn)
-- The barrel stands on its origin while the body is centred on its own.
natiny.entity.set_parent(model, body)
natiny.entity.set_position(model, natiny.entity.SPACE_LOCAL, 0, -BARREL.height / 2, 0)
natiny.physics.set_material(body, 0.5, 0.1, SHOT.density)
natiny.physics.set_interpolation(body, true)
natiny.physics.set_velocity(body, dx * SHOT.speed, dy * SHOT.speed, dz * SHOT.speed)
barrels[#barrels + 1] = { body = body, model = model }
if #barrels > SHOT.limit then
clear({ table.remove(barrels, spare()) })
end
end
local MOUSE_LEFT = natiny.input.MOUSE_BUTTON_LEFT
local KEY_R = natiny.input.KEY_R
local function frame(dt)
natiny.window.bind(win, function()
local captured = natiny.input.get_mouse_captured()
if captured then
yaw = yaw - natiny.input.get_mouse_x_speed() * CAMERA.sensitivity
pitch = pitch - natiny.input.get_mouse_y_speed() * CAMERA.sensitivity
pitch = math.max(-60, math.min(30, pitch))
natiny.entity.set_rotation(camera, natiny.entity.SPACE_WORLD, pitch, yaw, 0)
end
-- The click that takes the mouse does not also fire: a barrel spawned
-- by it would come as a surprise.
if natiny.input.get_mouse_button_pressed(MOUSE_LEFT) then
if captured then
local ry, rx = math.rad(yaw), math.rad(pitch)
shoot(-math.sin(ry) * math.cos(rx),
math.sin(rx),
-math.cos(ry) * math.cos(rx))
else
natiny.input.set_mouse_capture(true)
end
end
if natiny.input.get_key_pressed(KEY_R) then
reset()
end
natiny.render.clear(BG)
natiny.camera.bind(camera, function()
natiny.render.tag("model")
end)
end)
end
local function load_mesh(path)
local resource = natiny.resource.load(path)
local mesh = natiny.mesh.load(resource)
natiny.resource.destroy(resource)
return mesh
end
-- One mesh, used twice: drawn as a model and collided with triangle for
-- triangle, so the crates rest on the platform they look to be on.
local platform_mesh = load_mesh("common/models/platform.glb")
natiny.model.create(platform_mesh)
natiny.physics.mesh(platform_mesh)
barrel_mesh = load_mesh("common/models/barrel.glb")
box_mesh = load_mesh("common/models/box_01.glb")
build_stack()
natiny.backend.loop(function(dt)
frame(dt)
end)
natiny.backend.shutdown()