Lots of problems with this.
1. Using non-integer values for map position. You should use setWorldPosition() for that.
2. Not accounting for variable framerate. You should use Time.deltaTime() to move the object by the correct amount each frame.
3. Using party facing when it can clearly change in the middle of everything.
This is my object transformation code atm, lots of stuff is unfinished (rotation is unfinished, callbackpos in moveObjectDistance is unimplemented, callbacks are not called in the correct order) but it will accomplish what you are trying to do. The function you're interested in is moveObjectDistance().
- translate(), rotate(), and scale() if you want instant translation/rotation/scaling
- moveObjectDistance() and rotateAngles() if you want smooth translation/rotation
Code: Select all
-- Get rotation matrices that we can use for temporary storage. Here are some
-- properties of rotation matrices in Grimrock that you may not expect:
-- - GameObject:getWorldRotation() returns a COPY.
-- - According to a grimrock.net blog post, there's a "pool" of matrices so that
-- new ones aren't constantly being allocated.
-- To get away with these two matrices, we must follow a specific discipline:
-- assume scratchMatrix is invalidated after you call any function EXCEPT
-- for matrixMultiply.
scratchMatrix = {x={},y={},z={},w={}}
do
local s = self.go:getWorldRotation()
for c=1,4 do
scratchMatrix.x[c] = s.x[c]
scratchMatrix.y[c] = s.y[c]
scratchMatrix.z[c] = s.z[c]
scratchMatrix.w[c] = s.w[c]
end
end
--------------------------------------------------------------------------------
-- SIMPLE STUFF (spawn, destroy, set non-world position, etc.)
--------------------------------------------------------------------------------
-- spawn entity name at same position as on with id id
function spawnOn(on,name,id)
return spawn(name,on.level,on.x,on.y,on.facing,on.elevation,id)
end
-- spawn entity name at same position *and* world position as on with id id
function spawnOnExact(on,name,id)
local spawned = spawn(name,on.level,on.x,on.y,on.facing,on.elevation,id)
spawned:setWorldPosition(on:getWorldPosition())
return spawned
end
-- spawn entity name at same position *and* world position *and* world rotation as on with id id
function spawnOnExactWithRotation(on,name,id)
local spawned = spawn(name,on.level,on.x,on.y,on.facing,on.elevation,id)
spawned:setWorldPosition(on:getWorldPosition())
spawned:setWorldRotation(on:getWorldRotation())
return spawned
end
-- set world position taking a table instead of a vector
function setWorldPositionTable(gameObject,position)
gameObject:setWorldPosition(vec(position[1],position[2],position[3],position[4] or 0))
end
-- a version of setDoorState that...actually works
-- TODO: obsolete in 2.1.19, lol
function safeSetDoorState(gameObject,doorComponentName,state)
local door = gameObject[doorComponentName]
if state == "closed" then
local closeSound, closeVelocity, lockSound = door:getCloseSound(), door:getCloseVelocity(), door:getLockSound()
door:setCloseSound("silence") door:setCloseVelocity(-math.huge) door:setLockSound("silence")
door.go.controller:close()
game.frame.callNextFrame(safeSetDoorStateCleanup,{gameObject.id,doorComponentName,state,closeSound,closeVelocity,lockSound})
elseif state == "open" then
local openSound, openVelocity, lockSound = door:getOpenSound(), door:getOpenVelocity(), door:getLockSound()
door:setOpenSound("silence") door:setOpenVelocity(math.huge) door:setLockSound("silence")
door.go.controller:open()
game.frame.callNextFrame(safeSetDoorStateCleanup,{gameObject.id,doorComponentName,state,openSound,openVelocity,lockSound})
else
debug.exceptions.err("invalid door state ("..state..") in safeSetDoorState for "..gameObject.id.."."..doorComponentName)
end
end
function safeSetDoorStateCleanup(gameObjectId, doorComponentName, state, sound, velocity, lockSound)
local door = findEntity(gameObjectId)[doorComponentName]
door:setLockSound(lockSound)
if state == "closed" then
door:setCloseSound(sound) door:setCloseVelocity(velocity)
elseif state == "open" then
door:setOpenSound(sound) door:setOpenVelocity(velocity)
else
debug.exceptions.err("invalid door state ("..state..") in safeSetDoorStateCleanup for "..gameObjectId.."."..doorComponentName)
end
end
function setFacing(gobj,facing)
gobj:setPosition(gobj.x,gobj.y,facing,gobj.elevation,gobj.level)
end
function setX(gobj,x)
gobj:setPosition(x,gobj.y,gobj.facing,gobj.elevation,gobj.level)
end
function setY(gobj,y)
gobj:setPosition(gobj.x,y,gobj.facing,gobj.elevation,gobj.level)
end
function setElevation(gobj,e)
gobj:setPosition(gobj.x,gobj.y,gobj.facing,e,gobj.level)
end
-- like setPosition but leave arguments nil to keep original position
function setPositionPartial(gobj,x,y,facing,elevation,level)
gobj:setPosition(x or gobj.x,y or gobj.y,facing or gobj.facing,elevation or gobj.elevation,level or gobj.level)
end
-- call gobj:destroy()
function destroy(gobj)
gobj:destroy()
end
-- call gobj:destroy() and return true if gobj is not nil or false
-- do nothing and return false if gobj is nil or false
function safeDestroy(gobj)
if gobj then
gobj:destroy()
return true
else
return false
end
end
--------------------------------------------------------------------------------
-- TRANSFORMATION
--------------------------------------------------------------------------------
-- These functions are aimed at translating, rotating, and scaling GameObjects.
-- They change only world position and world rotation.
-- Performance here is not especially good, particularly for rotation, so don't
-- use on too many objects at once.
X = 1
Y = 2
Z = 3
W = 4
-- Table of moving GameObjects and corresponding data.
gameObjects = {}
-- Translate a game object's world position.
-- TODO: call callbacks in correct order.
function translate(gobj, x, y, z)
local pos = gobj:getWorldPosition()
local gid = gobj.id
local gData = gameObjects[gid]
local nx = pos[X]+(x or 0)
local ny = pos[Y]+(y or 0)
local nz = pos[Z]+(z or 0)
if gData then
local e = gData.endpoints
local v = gData.velocities
local c = gData.callbacks
local callx = false
local cally = false
local callz = false
if e then
-- reaching an endpoint; velocityCleanup will handle the rest
if e[X] and ((pos[X] <= e[X] and e[X] <= nx) or (pos[X] >= e[X] and e[X] >= nx)) then
nx = e[X]
if v then
v[X] = nil
if c and c[X] then
callx = true
end
end
end
if e[Y] and ((pos[Y] <= e[Y] and e[Y] <= ny) or (pos[Y] >= e[Y] and e[Y] >= ny)) then
ny = e[Y]
if v then
v[Y] = nil
if c and c[Y] then
cally = true
end
end
end
if e[Z] and ((pos[Z] <= e[Z] and e[Z] <= nz) or (pos[Z] >= e[Z] and e[Z] >= nz)) then
nz = e[Z]
if v then
v[Z] = nil
if c and c[Z] then
callz = true
end
end
end
end
gobj:setWorldPosition(vec(nx,ny,nz,pos[4]))
if callx then
c[X][1](gobj,c[X][2]) -- call callback with args
c[X] = nil
end
if cally then
c[Y][1](gobj,c[Y][2]) -- call callback with args
c[Y] = nil
end
if callz then
c[Z][1](gobj,c[Z][2]) -- call callback with args
c[Z] = nil
end
else -- if we dont have to worry about endpoints or callbacks, the code is somewhat shorter.
gobj:setWorldPosition(vec(nx,ny,nz,pos[4]))
end
end
-- Update moving objects. Should be called once per frame.
function update()
local dtime = Time.deltaTime()
for gobjId,data in pairs(gameObjects) do
local d = {[X]=0,[Y]=0,[Z]=0}
local ad = {[X]=nil,[Y]=nil,[Z]=nil}
local v = data.velocities
local a = data.accelerations
local av = data.angularVelocities
for dim = X, Z do
if v and v[dim] then
if a and a[dim] then
-- In case you failed both physics and calculus, this formula will
-- only work for constant acceleration.
d[dim] = (v[dim]+0.5*a[dim]*dtime)*dtime
v[dim] = v[dim]+a[dim]*dtime
else
d[dim] = v[dim]*dtime
end
end
if av and av[dim] then
ad[dim] = av[dim]*dtime
end
end
if v then translate(findEntity(gobjId),d[X],d[Y],d[Z]) end
if av then rotate(findEntity(gobjId),ad[X],ad[Y],ad[Z]) end
velocityCleanup(gobjId)
end
end
-- Here's where things get kind of sucky. I do not know of any way to create new
-- matrices other than acquiring them via GameObject:getWorldRotation(), which
-- creates a new copy of the matrix every time.
-- TODO: call callbacks in correct order
function rotate(gobj,dX,dY,dZ)
local gid = gobj.id
local gData = gameObjects[gid]
local mat = gobj:getWorldRotation()
local av = gData and gData.angularVelocities
local adr = gData and gData.angularDistanceRemaining
local c = gData and gData.angularCallbacks
local callx = false
local cally = false
local callz = false
if dX then
if adr then
local remaining = adr[X]
if remaining then
if (dX >= 0 and dY >= remaining) or (dY < 0 and dY <= remaining) then
dX = remaining
adr[X] = nil
av[X] = nil
if c and c[X] then
callx = true
end
else
adr[X] = remaining-dX
end
end
end
createRotationMatrixX(scratchMatrix,dX)
matrixMultiply(mat,scratchMatrix)
end
if dY then
if adr then
local remaining = adr[Y]
if remaining then
if (dY >= 0 and dY >= remaining) or (dY < 0 and dY <= remaining) then
dY = remaining
adr[Y] = nil
av[Y] = nil
if c and c[Y] then
cally = true
end
else
adr[Y] = remaining-dY
end
end
end
createRotationMatrixY(scratchMatrix,dY)
matrixMultiply(mat,scratchMatrix)
end
if dZ then
if adr then
local remaining = adr[Z]
if remaining then
if (dZ >= 0 and dZ >= remaining) or (dZ < 0 and dZ <= remaining) then
dZ = remaining
adr[Z] = nil
av[Z] = nil
if c and c[Z] then
callz = true
end
else
adr[Z] = remaining-dZ
end
end
end
createRotationMatrixZ(scratchMatrix,dZ)
matrixMultiply(mat,scratchMatrix)
end
gobj:setWorldRotation(mat)
if callx then
c[X][1](gobj,c[X][2]) -- call callback with args
c[X] = nil
end
if cally then
c[Y][1](gobj,c[Y][2]) -- call callback with args
c[Y] = nil
end
if callz then
c[Z][1](gobj,c[Z][2]) -- call callback with args
c[Z] = nil
end
end
function scale(gobj,dX,dY,dZ)
local gid = gobj.id
local gData = gameObjects[gid]
local mat = gobj:getWorldRotation()
scratchMatrix.x[1] = dX
scratchMatrix.x[2] = 0
scratchMatrix.x[3] = 0
scratchMatrix.x[4] = 0
scratchMatrix.y[1] = 0
scratchMatrix.y[2] = dY
scratchMatrix.y[3] = 0
scratchMatrix.y[4] = 0
scratchMatrix.z[1] = 0
scratchMatrix.z[2] = 0
scratchMatrix.z[3] = dZ
scratchMatrix.z[4] = 0
scratchMatrix.w[1] = 0
scratchMatrix.w[2] = 0
scratchMatrix.w[3] = 0
scratchMatrix.w[4] = 1
matrixMultiply(mat,scratchMatrix)
gobj:setWorldRotation(mat)
end
-- Set angular velocity of an object. x/y/z can be nil or 0 to leave alone.
function setAngularVelocity(gobj,x,y,z)
local gobjId = gobj.id
if not gameObjects[gobjId] then gameObjects[gobjId] = {} end
local gData = gameObjects[gobjId]
if not gData.angularVelocities then
local hasX = x and x ~= 0
local hasY = y and y ~= 0
local hasZ = z and z ~= 0
if hasX or hasY or hasZ then
gData.angularVelocities = {
[X]=hasX and x,
[Y]=hasY and y,
[Z]=hasZ and z
}
end
else
gData.angularVelocities[X] = x
gData.angularVelocities[Y] = y
gData.angularVelocities[Z] = z
end
end
-- Set angular distance remaining of an object. x/y/z can be nil or 0 to leave alone.
function setAngularDistanceRemaining(gobj,x,y,z)
local gobjId = gobj.id
local gData = gameObjects[gobjId]
if not gData.angularDistanceRemaining then
local hasX = x and x ~= 0
local hasY = y and y ~= 0
local hasZ = z and z ~= 0
if hasX or hasY or hasZ then
gData.angularDistanceRemaining = {
[X]=hasX and x,
[Y]=hasY and y,
[Z]=hasZ and z
}
end
else
gData.angularDistanceRemaining[X] = x
gData.angularDistanceRemaining[Y] = y
gData.angularDistanceRemaining[Z] = z
end
end
function createRotationMatrixX(mat, angle)
local sin = math.sin(angle)
local cos = math.cos(angle)
mat.x[1] = 1
mat.x[2] = 0
mat.x[3] = 0
mat.y[1] = 0
mat.y[2] = cos
mat.y[3] = sin
mat.z[1] = 0
mat.z[2] = -sin
mat.z[3] = cos
--return mat
end
function createRotationMatrixY(mat, angle)
local sin = math.sin(angle)
local cos = math.cos(angle)
mat.x[1] = cos
mat.x[2] = 0
mat.x[3] = -sin
mat.y[1] = 0
mat.y[2] = 1
mat.y[3] = 0
mat.z[1] = sin
mat.z[2] = 0
mat.z[3] = cos
--return mat
end
function createRotationMatrixZ(mat, angle)
local sin = math.sin(angle)
local cos = math.cos(angle)
mat.x[1] = cos
mat.x[2] = sin
mat.x[3] = 0
mat.y[1] = -sin
mat.y[2] = cos
mat.y[3] = 0
mat.z[1] = 0
mat.z[2] = 0
mat.z[3] = 1
--return mat
end
-- multiply 4x4 matrices (or tables) mat1 and mat2, place result in mat1
-- mat2 will be unchanged
function matrixMultiply(mat1,mat2)
-- XXX: I'm sure this can be optimized better, but it's Lua so using the
-- "good" algorithms may not help.
-- currently the final column is not copied since as soon as it's
-- overwritten, the original is no longer needed; we could, of course,
-- save on storage by making more assignments...
-- These are intentionally permanent variables. Why? This function is
-- called EXTREMELY often. (That's also why we don't use a table.) This
-- code is virtually impossible to profile, but based on my limited
-- knowledge of LuaJIT I am around 90% sure this is faster than using
-- temporary variables in this case.
scratchx1 = mat1.x[1]
scratchx2 = mat1.x[2]
scratchx3 = mat1.x[3]
scratchy1 = mat1.y[1]
scratchy2 = mat1.y[2]
scratchy3 = mat1.y[3]
scratchz1 = mat1.z[1]
scratchz2 = mat1.z[2]
scratchz3 = mat1.z[3]
scratchw1 = mat1.w[1]
scratchw2 = mat1.w[2]
scratchw3 = mat1.w[3]
for i = 1,4 do
mat1.x[i] = scratchx1 * mat2.x[i]
+ scratchx2 * mat2.y[i]
+ scratchx3 * mat2.z[i]
+ mat1.x[4] * mat2.w[i]
mat1.y[i] = scratchy1 * mat2.x[i]
+ scratchy2 * mat2.y[i]
+ scratchy3 * mat2.z[i]
+ mat1.y[4] * mat2.w[i]
mat1.z[i] = scratchz1 * mat2.x[i]
+ scratchz2 * mat2.y[i]
+ scratchz3 * mat2.z[i]
+ mat1.z[4] * mat2.w[i]
mat1.w[i] = scratchw1 * mat2.x[i]
+ scratchw2 * mat2.y[i]
+ scratchw3 * mat2.z[i]
+ mat1.w[4] * mat2.w[i]
end
--return mat1
end
-- Set velocity of an object. x/y/z can be nil to leave alone.
function setVelocity(gobj,x,y,z)
local gobjId = gobj.id
if not gameObjects[gobjId] then gameObjects[gobjId] = {} end
local gData = gameObjects[gobjId]
if not gData.velocities then
local hasX = x and x ~= 0
local hasY = y and y ~= 0
local hasZ = z and z ~= 0
if hasX or hasY or hasZ then
gData.velocities = {
[X]=hasX and x or nil,
[Y]=hasY and y or nil,
[Z]=hasZ and z or nil
}
end
else
gData.velocities[X] = x
gData.velocities[Y] = y
gData.velocities[Z] = z
velocityCleanup(gobjId)
end
end
-- Modify velocity of an object. x/y/z can be 0 to leave alone.
function modifyVelocity(gobj,x,y,z)
local gobjId = gobj.id
if not gameObjects[gobjId] then gameObjects[gobjId] = {} end
local gData = gameObjects[gobjId]
if not gData.velocities then
local hasX = x ~= 0
local hasY = y ~= 0
local hasZ = z ~= 0
if hasX or hasY or hasZ then
gData.velocities = {
[X]=hasX and x or nil,
[Y]=hasY and y or nil,
[Z]=hasZ and z or nil
}
end
else
gData.velocities[X] = (gData.velocities[X] or 0)+x
gData.velocities[Y] = (gData.velocities[Y] or 0)+y
gData.velocities[Z] = (gData.velocities[Z] or 0)+z
velocityCleanup(gobjId)
end
end
-- Move an object a certain distance on each axis with specified velocities,
-- canceling any existing movement *and canceling existing callbacks without calling them*.
-- gobj: object to move
-- move: table with info about how to move the object. Supports the following fields:
-- dx, dy, dz: distance to move each axis (can be nil)
-- vx, vy, vz: velocity to move each axis (sign should match corresponding distance)
-- callbackx, callbacky, callbackz: functions to call when finished moving on axis (can be nil), first argument passed will be the object
-- callbackxargs, callbackyargs, callbackzargs: additional arguments to pass to callbacks (can be nil)
-- callbackpos: function to call when object's map position changes (can be nil), i.e. it crosses a tile boundary, first argument passed will be the object,
-- then the object's *previous* x,y,elevation as the second, third, and fourth arguments.
-- callbackposargs: additional arguments to pass to callbackpos (can be nil)
function moveObjectDistance(gobj,move)
local gobjId = gobj.id
setVelocity(gobj,move.vx,move.vy,move.vz)
setEndpointsDelta(gobj,move.dx,move.dy,move.dz)
gameObjects[gobjId].accelerations = nil
if (move.dx and move.callbackx) or (move.dy and move.callbacky) or (move.dz and move.callbackz) or (move.callbackpos) then
gameObjects[gobjId].callbacks = {[X] = {move.callbackx,move.callbackxargs}, [Y] = {move.callbacky,move.callbackyargs}, [Z] = {move.callbackz,move.callbackzargs}, ["callbackpos"] = {move.callbackpos,move.callbackposargs}}
else
gameObjects[gobjId].callbacks = nil -- clear existing
end
end
-- WARNING XXX TODO NOTE ETC: the whole rotation code is completely broken
-- TODO: make this not completely useless for multiple axes (the framerate will change the
-- rotation as the axes are applied individually).
-- Rotate an object a certain distance (degrees) on each axis with specified velocities,
-- canceling any existing rotation.
-- Note that currently this is pretty inefficient, so don't rotate too many objects at once.
-- gobj: object to move
-- dx, dy, dz: angle to rotate each axis
-- tx, ty, tz: time for rotation on each axis to complete
-- callbackx, callbacky, callbackz: functions to call when finished moving on axis (can be nil), first argument passed will be the object
-- callbackxargs, callbackyargs, callbackzargs: additional arguments to pass to callbacks (can be nil)
function rotateAngles(gobj,dx,dy,dz,tx,ty,tz,callbackx,callbackxargs,callbacky,callbackyargs,callbackz,callbackzargs)
local gobjId = gobj.id
local avx = nil
local avy = nil
local avz = nil
if dx then avx = dx/tx end
if dy then avy = dy/ty end
if dz then avz = dz/tz end
setAngularVelocity(gobj,avx,avy,avz)
setAngularDistanceRemaining(gobj,dx,dy,dz)
velocityCleanup(gobjId)
if (dx and callbackx) or (dy and callbacky) or (dz and callbackz) then
gameObjects[gobjId].angularCallbacks = {[X] = {callbackx,callbackxargs}, [Y] = {callbacky,callbackyargs}, [Z] = {callbackz,callbackzargs}}
end
end
-- Set the endpoints of an object relative to its current position. Cancels
-- existing endpoints.
function setEndpointsDelta(gobj,dx,dy,dz)
local pos = gobj:getWorldPosition()
local points = {
[X] = (dx and dx ~= 0 and pos[X]+dx) or nil,
[Y] = (dy and dy ~= 0 and pos[Y]+dy) or nil,
[Z] = (dz and dz ~= 0 and pos[Z]+dz) or nil
}
if not gameObjects[gobj.id] then
gameObjects[gobj.id] = {endpoints = points}
else
gameObjects[gobj.id].endpoints = points
end
end
-- remove 0 velocities and corresponding endpoints
-- if channel has no or 0 velocity and no acceleration, will be removed
-- if all channels removed, object is freed
function velocityCleanup(gobjId)
local gData = gameObjects[gobjId]
if gData and findEntity(gobjId) then
local v = gData.velocities
local a = gData.accelerations
local av = gData.angularVelocities
if av then
local e = gData.angularDistanceRemaining
for i = X, Z do
if (not av[i]) or av[i] == 0 then
av[i] = nil
if e then e[i] = nil end
end
end
if not (av[X] or av[Y] or av[Z]) then
gData.angularVelocities = nil
gData.angularDistanceRemaining = nil
if not v then
gameObjects[gobjId] = nil
end
end
end
if v and not a then
local e = gData.endpoints
for i = X, Z do
if (not v[i]) or v[i] == 0 then
v[i] = nil
if e then e[i] = nil end
end
end
if not (v[X] or v[Y] or v[Z]) then
gData.velocities = nil
if not gData.angularVelocities then
gameObjects[gobjId] = nil
end
end
end
else
gameObjects[gobjId] = nil
end
end
This moves the object "gazingStatue" 8 meters down at a speed of 1.6 m/s, and at the end, calls destroyStatue with the gazingStatue object as the only argument. Note that callbacks are referenced from the script with the translation code, which means that you should never try to use the local environment, only the function's arguments and global variables (
).