I’m trying to make an endless runner using voxels.
This code runs fine on my iPhone 16e once.
The second time I run it, the entire Codea app crashes fully almost immediately.
There’s apparently a memory leak happening between runs, right? Something Codea doesn’t fully clean up when a project quits to the editor.
--# Main
require(asset.documents.Craft.Cameras)
require(asset.documents.Craft.Block_Library)
function setup()
collectgarbage("collect")
scene = craft.scene()
blocks()
scene.sun.rotation = quat.eulerAngles(25, 125, 0)
scene.ambientColor = color(96, 96, 96, 255)
scene.sun:get(craft.light).intensity = 0.6
skyColor = color(0, 134, 255, 255)
scene.fogEnabled = true
scene.fogColor = skyColor
grassID = scene.voxels.blocks.grass.id
dirtID = scene.voxels.blocks.dirt.id
VOL_W = 48
VOL_H = 10
VOL_D = 16
AHEAD = 9
BEHIND = 2
BASE = 2 -- lowest surface height inside a volume
HILL = 6 -- height variation above BASE (BASE+HILL must be < VOL_H)
heightNoise = craft.noise.perlin()
heightNoise.octaves = 3
heightNoise.frequency = 0.02
heightNoise.seed = 1
scene.fogNear = AHEAD * VOL_D * 0.6
scene.fogFar = AHEAD * VOL_D
runner = vec3(VOL_W/2 + 0.5, BASE + 1, BEHIND * VOL_D)
footY = sampleHeight(math.floor(runner.x), math.floor(runner.z)) + 1
footVY = 0
camGroundY = footY
pool = {}
for i = 0, AHEAD + BEHIND - 1 do
local e = scene:entity()
e.position = vec3(0, 0, i * VOL_D)
local v = e:add(craft.volume, VOL_W, VOL_H, VOL_D)
fillTerrain(v, i * VOL_D)
table.insert(pool, { entity = e, volume = v, row = i })
end
parameter.watch("memKB") -- add in setup
setupWalkParameters()
ArmSwing = 72; LegSwing = 75; WalkSpeed = 10
rig = adventurerRig(scene)
parameter.number("RunSpeed", 0, 40, 40)
parameter.number("CamHeight", 2, 100, 13.56)
parameter.number("CamBack", 2, 100, 20)
parameter.number("CamPitch", -90, 90, 17.37)
parameter.number("CamYaw", -180, 180, 0)
parameter.number("CamSmooth", 0, 10, 1.5)
parameter.number("StepUp", 0, 30, 12)
parameter.number("Gravity", 0, 80, 30)
parameter.number("AvatarScale", 0.01, 8, 0.1)
parameter.number("AvatarLift", -2, 10, 0.2)
parameter.number("AvatarYaw", -180, 180, 180)
end
-- surface height at a world cell (same function the avatar's feet use)
function sampleHeight(wx, wz)
local n = heightNoise:getValue(wx, 0, wz) -- ~ -1..1
return BASE + math.floor((n + 1) * 0.5 * HILL)
end
-- thin shell: grass at the surface, 2 dirt below, empty elsewhere
function fillTerrain(v, wz0)
for x = 0, VOL_W-1 do
for z = 0, VOL_D-1 do
local h = sampleHeight(x, wz0 + z)
for y = 0, VOL_H-1 do
if y == h then
v:set(x, y, z, BLOCK_ID, grassID)
elseif y >= h-2 and y < h then
v:set(x, y, z, BLOCK_ID, dirtID)
else
v:set(x, y, z, BLOCK_ID, 0) -- clears old content on recycle
end
end
end
end
end
function update(dt)
runner.z = runner.z + RunSpeed * dt
local maxRow = -1e9
for _, p in ipairs(pool) do
if p.row > maxRow then maxRow = p.row end
end
local behindZ = runner.z - BEHIND * VOL_D
for _, p in ipairs(pool) do
if (p.row * VOL_D) + VOL_D < behindZ then
maxRow = maxRow + 1
p.row = maxRow
p.entity.position = vec3(0, 0, p.row * VOL_D)
fillTerrain(p.volume, p.row * VOL_D) -- refill with terrain at the new spot
-- in update, inside the recycle loop, right after fillTerrain(...):
collectgarbage("collect")
end
end
-- ground under the runner, from the same noise
local groundY = sampleHeight(math.floor(runner.x), math.floor(runner.z)) + 1
-- step up quickly, fall under gravity going down
if groundY > footY then
footY = footY + (groundY - footY) * (1 - math.exp(-StepUp * dt))
footVY = 0
else
footVY = footVY - Gravity * dt
footY = footY + footVY * dt
if footY <= groundY then footY = groundY; footVY = 0 end
end
camGroundY = camGroundY + (groundY - camGroundY) * (1 - math.exp(-CamSmooth * dt))
rig.root.position = vec3(runner.x, footY + AvatarLift, runner.z)
rig.root.rotation = quat.eulerAngles(0, AvatarYaw, 0)
rig.root.scale = vec3(AvatarScale, AvatarScale, AvatarScale)
scene.camera.position = vec3(runner.x, camGroundY + CamHeight, runner.z - CamBack)
scene.camera.rotation = quat.eulerAngles(CamPitch, CamYaw, 0)
animateWalk(rig, ElapsedTime)
scene:update(dt)
-- at the very end of update:
memKB = math.floor(collectgarbage("count"))
end
function draw()
update(DeltaTime)
scene:draw()
end
--# Avatar
BODY_PARTS = {
head = {13,14,15,16,17,18,19,20,21,22,23,24},
torso = {1,2,3,4,5,6,7,8,9,10,11,12,49,52},
leftArm = {25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77},
rightArm = {78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131},
leftLeg = {132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167},
rightLeg = {168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203}
}
function setupWalkParameters()
local function add(name, minV, maxV, defaultV)
local startValue = readLocalData(name, defaultV)
_G[name] = startValue
parameter.number(name, minV, maxV, startValue, function(v)
_G[name] = v
saveLocalData(name, v)
end)
end
add("ArmPivotX", 0, 1, 0.38)
add("ArmPivotY", -1, 15, 10.66)
add("ArmPivotZ", -1, 1, 0)
add("LegPivotX", 0, 1, 0.18)
add("LegPivotY", -1, 15, 6.86)
add("LegPivotZ", -1, 1, 0)
add("ArmSwing", 0, 90, 31.20)
add("LegSwing", 0, 90, 38.95)
add("WalkSpeed", 0, 10, 3)
end
function adventurerRig(scene)
local model = craft.model(asset.builtin.Blocky_Characters.Adventurer)
local material = craft.material(asset.builtin.Materials.Standard)
material.map = readImage(asset.builtin.Blocky_Characters.AdventurerSkin)
local ind = model.indices
local triangles = {}
for i = 1, #ind, 3 do
triangles[#triangles+1] = { i1 = ind[i], i2 = ind[i+1], i3 = ind[i+2] }
end
local function buildMesh(triIndices)
local pos, uv, col, norm = model.positions, model.uvs, model.colors, model.normals
local newPos, newUV, newCol, newNorm, newInd, vertexMap = {}, {}, {}, {}, {}, {}
local function addVertex(old)
if vertexMap[old] then return vertexMap[old] end
local new = #newPos + 1
vertexMap[old] = new
newPos[new] = pos[old]
if uv then newUV[new] = uv[old] end
if col then newCol[new] = col[old] end
if norm then newNorm[new] = norm[old] end
return new
end
for _, triId in ipairs(triIndices) do
local t = triangles[triId]
if t then
table.insert(newInd, addVertex(t.i1))
table.insert(newInd, addVertex(t.i2))
table.insert(newInd, addVertex(t.i3))
end
end
local m = craft.model()
m.positions = newPos
m.indices = newInd
if #newUV > 0 then m.uvs = newUV end
if #newCol > 0 then m.colors = newCol end
if #newNorm > 0 then m.normals = newNorm end
return m
end
local root = scene:entity()
local torsoPivot = scene:entity(); torsoPivot.parent = root
local headPivot = scene:entity(); headPivot.parent = torsoPivot
local leftArmPivot = scene:entity(); leftArmPivot.parent = torsoPivot
local rightArmPivot = scene:entity(); rightArmPivot.parent = torsoPivot
local leftLegPivot = scene:entity(); leftLegPivot.parent = torsoPivot
local rightLegPivot = scene:entity(); rightLegPivot.parent = torsoPivot
local meshes = {}
for name, triIndices in pairs(BODY_PARTS) do
local e = scene:entity()
e.model = buildMesh(triIndices)
e.material = material
meshes[name] = e
end
meshes.torso.parent = torsoPivot
meshes.head.parent = headPivot
meshes.leftArm.parent = leftArmPivot
meshes.rightArm.parent = rightArmPivot
meshes.leftLeg.parent = leftLegPivot
meshes.rightLeg.parent = rightLegPivot
leftArmPivot.position = vec3( 0.38, 0.40, 0)
rightArmPivot.position = vec3(-0.38, 0.40, 0)
leftLegPivot.position = vec3( 0.18,-0.50, 0)
rightLegPivot.position = vec3(-0.18,-0.50, 0)
headPivot.position = vec3(0,0.72,0)
meshes.leftArm.position = -leftArmPivot.position
meshes.rightArm.position = -rightArmPivot.position
meshes.leftLeg.position = -leftLegPivot.position
meshes.rightLeg.position = -rightLegPivot.position
meshes.head.position = -headPivot.position
return {
root = root,
torsoPivot = torsoPivot, headPivot = headPivot,
leftArmPivot = leftArmPivot, rightArmPivot = rightArmPivot,
leftLegPivot = leftLegPivot, rightLegPivot = rightLegPivot,
meshes = meshes
}
end
function animateWalk(rig, t)
local s = math.sin(t * WalkSpeed)
rig.leftArmPivot.position = vec3( ArmPivotX, ArmPivotY, ArmPivotZ )
rig.rightArmPivot.position = vec3(-ArmPivotX, ArmPivotY, ArmPivotZ )
rig.leftLegPivot.position = vec3( LegPivotX, LegPivotY, LegPivotZ )
rig.rightLegPivot.position = vec3(-LegPivotX, LegPivotY, LegPivotZ )
rig.meshes.leftArm.position = -rig.leftArmPivot.position
rig.meshes.rightArm.position = -rig.rightArmPivot.position
rig.meshes.leftLeg.position = -rig.leftLegPivot.position
rig.meshes.rightLeg.position = -rig.rightLegPivot.position
rig.meshes.head.position = -rig.headPivot.position
rig.leftArmPivot.rotation = quat.eulerAngles( s * ArmSwing, 0, 0)
rig.rightArmPivot.rotation = quat.eulerAngles(-s * ArmSwing, 0, 0)
rig.leftLegPivot.rotation = quat.eulerAngles(-s * LegSwing, 0, 0)
rig.rightLegPivot.rotation = quat.eulerAngles( s * LegSwing, 0, 0)
end
