Stormworks: Build and Rescue

Stormworks: Build and Rescue

38 vurderinger
Some useful Lua functions to use in your code (general, autopilot/navigation, 3d/vectors)
Af Kernle Frizzle
This is a list of functions you can copy and paste into your code to perform various calculations, most of which I use personally when I make an autopilot or a waypoint navigation system. Explanations included.
4
2
   
Pris
Føj til foretrukne
Gjort til foretrukken
Fjern som foretrukken
How to use a function in Lua
(For those who haven't toyed around with functions in Lua or other languages like Python. If you have, then skip this section.)

While most of the general functions are still applicable, I now have a much more concise and compact set of vector specific functions for use with 3D in my Vectors, In Lua guide.

DISCLAIMER: In this guide I sometimes use ""s around the names of variables. This is just to emphasize the names and improve readability. It does not mean they're strings.

A function, also sometimes called a subprogram (I think), is a way to section off a small piece of code so that it can be "called" back later and used. Instead of manually writing out a certain computation every time that you have to perform it, you can write that computation only once under the label of a function, and then simply call that function every time you want to use that bit of code.

Here's an example:

Let's say you are making a script that at multiple points has to calculate a Celsius to Fahrenheit conversion. You could manually write out that conversion (x*9/5+32) at every point where the conversion has to be made, which can use a lot of valuable space and may also make the code harder to read, or you could define a function that performs this computation on the input and spits out the result.
--let's say this is part of the script you're working on. Normally you wouldn't want to use a function for this, as it's already as simple as can be, but for the sake of example we will. celsius = input.getNumber(1) fahrenheit = celsius*9/5+32 output.setNumber(1,fahrenheit) --Our function may look something like this: function ctof(x) return x*9/5+32 end --The name of the function can be anything, in this case "ctof," but its generally a good idea to keep your function names reasonably different from the stock library functions given in Lua.

What does "return" actually do?
When you're giving a function an output, you write "return x," x being either a number or a true/false (boolean) value. When calling the function later in the code, you receive this output by setting a variable equal to the function. like so:
fahrenheit = ctof(celsius)
"fahrenheit" will now be equal to x*9/5+32 from inside of the function, but because our input in the parentheses was the value "celsius," "fahrenheit" is equal to celsius*9/5+32. You essentially plug in a value into a function, and the function spits out the result.

However, functions aren't limited to just one input and one output.
--This is a function that takes in two inputs, x and y, and spits out two outputs, one being the result when you divide x by y and the other being a boolean (on/off) value detecting if you're dividing by 0. function divide(x,y) if y==0 then return 0, true else return x/y, false end end --When calling this function, instead of making one variable equal to the function you have to make two variables equal to the function. This is done in the same way the function is given two outputs: a=1 b=2 result, divideByZero = divide(a,b) --In this case, "result" would equal a/b, and "divideByZero" would equal false. Notice the order of the inputs and outputs is what determines the values of each. If I wrote it as divideByZero, result = divide(a,b) then "divideByZero" would equal a/b and "result" would equal false. Same goes for the inputs in the parentheses of the function.

Alternate Syntax
You may sometimes see a function defined in the form
functionName = function (parameters) --doing stuff return something end
This is the exact same as the classic function functionName(), just rearranged. This is only really allowed in Lua because it is what is called and "object oriented" language, i.e. pretty much everything is treated as an object or variable that can be shifted around and manipulated. This other syntax for defining a function makes it a bit more obvious that a function is just a funky variable that can perform tasks and change depending on an input parameter.

This also means that because functions are essentially objects, they can be used as parameters for other functions. If you do some research you may find a function written as
table.sort(table, function (a,b) return a>b end)
This definitely looks confusing and scary, but remember, functions themselves can be used as parameters. In that case, this syntax is favorable, as inside the predefined table.sort function, the parameter for that function (a,b) is given its own local name in exactly the same way as any other parameter. The only difference is, instead of that parameter being a variable with some value, it is a function.

None of that alternate syntax stuff is important for this guide though, everything is written in that normal form. I'm honestly not sure which is more normal and which is the "syntactic sugar," but I like the first way so I'm keeping it that way.

The indenting and returns aren't necessary in Lua, but it makes the code easier to read.

This should be everything you need to know before using the functions in the rest of this guide.
Big ol' block of text (functions for general and autopilot use)
(The indentation isn't required)

pi=math.pi pi2=pi*2
This is not only handy to have, but also needed for a lot of the functions. It saves space and makes trigonometric functions much easier to read. Every angle measure in this guide will also be measured in radians, not degrees. Or gradient if anyone actually uses that.
function sgn(x) if x<0 then return -1 else return 1 end end --or... function sgn(x) return x<0 and -1 or 1 end
This function is the same as the sgn() you may use in the microcontroller function box. It returns +1 when the input is greater or equal to 0, and returns -1 when it's less than 0.

The second version of sgn() only works because of how lua treats ands and ors. If the first argument of an and is false or nil, it will return that first argument's value, else it will return the second argument's value. or works similarly, only if the first argument's value is false or nil, it will return the second argument's value, else it returns the first argument's value. This if-statement type of logic makes it possible to optimize certain functions by avoiding having to perform a costly calculation if another condition is met, same as an if-then-else statement.
function clamp(x,l,u) return math.min(math.max(x,l),u) end
Same as sgn(), this is a function that's available in the function boxes in microcontrollers. It "clamps" the value x between l and u, representing the lower and upper values.
function len(x,y) return math.sqrt(x^2+y^2) end
Yet again, available in function boxes. It's the Pythagorean theorem in disguise.
function atan2(dy,dx) if dx>0 then return math.atan(dy/dx) elseif dx<0 then return math.atan(dy/dx)+pi*sgn(dy) else return 0 end end --Edit: I'm an idiot, apparently if you use the default math.atan() in this exact way it will output the value this function will. It's already in the game.
This function, while also technically being available in the function boxes, may work slightly differently (I've never used this in a function box). If you are familiar with the unit circle, atan2() will take in your x and y values and return the angle to those values (in radians). Keep in mind that angle 0 faces directly right (x), and the angle increases as you rotate CCW around the origin. If you want to use this to find your bearing to a waypoint, just pretend y is x and x is y, like so:
atan2(waypointX,waypointY)
(ensure these inputs are relative to your location, not just the map coordinates)
function norm(x) return x-pi2*math.floor(x/pi2) end function norm2(x) return x-pi2-pi2*math.floor((x-pi)/pi2) end --or... function norm(x) return x%pi2 end function norm2(x) return (x-pi)%pi2-pi end
These functions, norm() and norm2(), take in an angle (in radians) and return the same angle (in radians), but simplified. norm() restricts the output between 0 and pi2, norm2() restricts between -pi and +pi.

Elaboration:
Imagine a circle on a piece of paper. Draw a line from the center to the top edge of the circle, so the line is vertical. This is the reference for 0 degrees. Now imagine a second line, also from the center to the edge, but free to rotate around the circle like a hand of a clock. If you rotate the line so it faces directly to the right, you could say that it is at position +90 degrees. If you then rotate it further so it faces directly down, you could say that it is at position +180 degrees. If you keep rotating it in the same direction until it faces directly up, you could say it is at position +360 degrees. Now, if you rotate it to face directly right, you will be adding 90 degrees to 360 degrees, leaving you with +450 degrees. Here's the problem. You may be doing some calculation, like finding the difference between two angles, where you would want that angle to be equal to +90, not +450.

These two functions convert that +450 degrees with the extra 360 degrees into the +90 degrees by cutting off that extra 360, only instead of degrees, the input and output are in radians. The difference between norm() and norm2() is the bounds between which the angle is restricted. norm() will restrict the input angle between 0 and +2pi (a full 360 degrees), which is good for displays where you want to give information to a pilot or navigator. norm2() will restrict the input angle between -pi and +pi (-180 to 180). This function is very useful when paired with atan2(), as it lets you easily find the actual difference between two angles, even when one or both of the angles are offset by full rotations (multiples of pi2). Example:
out = norm2(atan2(wpX-gpsX,wpY-gpsY)-heading)
Because of the way angles add and subtract, this will return the real difference between the bearing towards a waypoint and the craft's current heading. This is the most important part of an autopilot.
You can also find the rate of change of angles with norm2(), which can be very useful for a compass. Example:
dcomp = norm2(comp-comp1) comp1=comp
dcomp will be the rate of change in radians per tick, which you can convert to other units with some simple math if you need. (Make sure you define comp1 as a number outside of onTick() by writing comp1=0, otherwise the game won't know what to do with comp-comp1 as comp1 wouldn't technically actually exist yet)

With a little creativity, you can also create course following autopilots and pretty much any waypoint system you could ever need.
Bigger ol' block of text (old 3d / coordinate transformation / polygons)
(The indentation isn't required)

function roll(ltlt,ftlt,utlt) local roll=math.asin(math.sin(ltlt)/math.cos(ftlt)) if utlt<0 then roll=pi-roll end return roll end
This function takes in the three tilt sensor axes, facing left, forward and up, and spits out the vehicle's true roll position. This is extremely important for any programs involving vehicle position or orientation, like radars or artificial horizons. Luckily a simple tilt sensor facing forward is enough for your pitch position. Like everything else this function returns in radians, and requires radians as it's input (just multiply the tilt sensor values by pi2). If the tilt sensors available to you are say facing right instead of left (cough cough physics sensor) all you have to do is make that tilt input negative; roll(-rtlt,ftlt,utlt) instead of roll(ltlt,ftlt,utlt). If a tilt up sensor isn't available (cough cough physics sensor) and you're not planning on flipping upsidedown (radars and such) then you can just input 1 for utlt. (or use the upcoming rotmat2d() along with the euler x, y and z to manually calculate the orientation)
function calcutlt(ltlt,ftlt) return math.asin(math.sqrt(1-math.sin(ltlt)^2-math.sin(ftlt)^2)) end
This honestly doesn't have to be a function, but I wanted to include it because I think it can be useful when saving space on the inputs and outputs of your controller. This function will calculate the value of a tilt sensor facing up based on the values of tilt sensors facing left and forward. Unlike the previous function, the sign of the inputs does not matter, so you can use right tilt or back tilt instead of left tilt or forward tilt if you have to.
function rot(x,y,z,a,e,r) qa=atan(x,y)+r qd=math.sqrt(x^2+y^2) x=qd*math.sin(qa) y=qd*math.cos(qa) qa=atan(y,z)+e qd=math.sqrt(y^2+z^2) y=qd*math.sin(qa) z=qd*math.cos(qa) qa=atan(x,z)+a qd=math.sqrt(x^2+z^2) x=qd*math.sin(qa) z=qd*math.cos(qa) return x,y,z end function arot(x,y,z,a,e,r) qa=atan(x,z)-a qd=math.sqrt(x^2+z^2) x=qd*math.sin(qa) z=qd*math.cos(qa) qa=atan(y,z)-e qd=math.sqrt(y^2+z^2) y=qd*math.sin(qa) z=qd*math.cos(qa) qa=atan(x,y)-r qd=math.sqrt(x^2+y^2) x=qd*math.sin(qa) y=qd*math.cos(qa) return x,y,z end
These two chonky ass functions are essentially manual matrix rotations using slow, ugly angles and trig. Input the x,y and z coordinates of a point in space, then input the angles you want to rotate that point around (in radians).
a is the azimuth angle, e is the elevation angle, and r is the roll angle (I don't know what the Greek letters are). The best way to think of how these work is to imagine a point in space in front of you. Imagine it slightly to the left and up from the centerline of your vision. The centerline of you vision is the base around which the point's coordinates are rotated. "r" will rotate it around the centerline, you can imagine the point spinning around the center of your vision. "a" will pan the point left and right along the horizontal plane, you can imagine the point spinning flat around your head. "e" will pan the point up and down. You can imagine the point moving up and down, up over your head and down under your feet. It is important to remember the order in which these operations are performed, as the "r" and "e" rotations take the centerline of your vision as the base around which to rotate. That's why there are two functions, rot() and arot().
rot() will directly rotate a point based on the rotations. arot() will undo these rotations. Both are very useful when creating a radar system, especially when your radar is rocking and rolling in the waves. rot() and arot() let you undo or redo the rotations your vessel might experience, returning the coordinates of a radar trace relative to the map rather than the vessel.
God I pity the lost soul who actually reads this.
Screw rot and arot, use this

function rotmat2d(x,y,r)
return x*math.cos(r)-y*math.sin(r), x*math.sin(r)+y*math.cos(r)
end
This is a much more condensed version of rot() and arot(), but instead of 3D it deals in 2D. It's also derived from matrices, so in theory it should be more efficient. As usual for math, x and y represent the x and y coordinates of a vector coming from the origin, and r is the angle in radians that the vector is rotated. The function returns x,y of the vector once the rotation is applied. +r will rotate the vector CCW, and -r will rotate CW. These really pull through in this demo.

If you have no orientation info other than the physics/astronomy sensor euler outputs, and you need angles like roll or pitch, use this guide. It has a section on calculating those values. It also has a section showing how to use rotmat2d() and the euler outputs to do the same job as rot() and arot(), using fewer characters.

If you want to use a shortcut for converting global coordinates to local coordinates by only having to calculate the trig once, check the "Vectors" section.



Polygon specific: (antiquated)

function inside(n)
t={}
for i=1,3 do
t[i]=atan2(n[i].x,n[i].z)
end
return sgn(norm(t[1]-t[2]))==sgn(norm(t[2]-t[3])) and sgn(norm(t[2]-t[3]))==sgn(norm(t[3]-t[1]))
end
--or:
function inside(t)
return sgn(cross(t[1],t[2]).z*cross(t[2],t[3]).z)==sgn(cross(t[2],t[3]).z*cross(t[3],t[1]).z) and sgn(cross(t[3],t[1]).z*cross(t[1],t[2]).z)==sgn(cross(t[2],t[3]).z*cross(t[3],t[1]).z)
end
This function is just for polygons. The input "n" has to be a table, in the form {{x=,y=,z=},{x=,y=,z=},{x=,y=,z=}} where each subtable..? is representing one vertex of the polygon. inside() will return true if you are standing directly above the triangle described by these coordinates. (The coordinates must be relative to the player / camera position)

function height(n)
n1=n[3].z-n[1].z
n2=n[3].x-n[1].x
n3=n[3].y-n[1].y
n4=n[2].z-n[1].z
A=((n[2].y-n[1].y)*n1-n3*n4)/((n[2].x-n[1].x)*n1-n2*n4)
B=(n3-A*n2)/n1
h=n[1].y-A*n[1].x-B*n[1].z
return A,B,h
end
--or:
function height(t,x,y)
local n=cross(vadd(t[2],t[1],-1),vadd(t[3],t[1],-1))
return -((x-t[1].x)*n.x+(y-t[1].y)*n.y)/n.z+t[1].z
end
Similar to inside(), height() takes the same input data configuration. height() will calculate the function which describes the polygon, and then calculates the X and Y gradients and your height above the triangle. The second version will take in a triangle t as well as x and y coordinates, and will then return the distance from (x,y,0) to the point on the triangle in front of it. Very useful for displaying and platforming in 3d space, and maybe raytracing if you're brave enough, but pretty much useless for everything else.
Vectors (further coordinate transforms)
All functions in this section assume vector quantities with the form {x=,y=,z=}. Luckily, these operations are universal, so it doesn't matter which axis each letter represents in the real world.
function mag(v) return math.sqrt(v.x^2+v.y^2+v.z^2) end function scal(v,s) return {x=v.x*s,y=v.y*s,z=v.z*s} end function vadd(a,b) return {x=a.x+b.x,y=a.y+b.y,z=a.z+b.z} end --or variable #args: function vadd(...) local s={x=0,y=0,z=0} for k,v in pairs({...}) do s={x=s.x+v.x,y=s.y+v.y,z=s.z+v.z} end return s end function dot(a,b) return a.x*b.x+a.y*b.y+a.z*b.z end --or combining dot and scal: function dot(a,b) if type(a)=="table" and type(b)=="table" then return a.x*b.x+a.y*b.y+a.z*b.z else local a=type(a)=="table" and a or {x=a,y=a,z=a} local b=type(b)=="table" and b or {x=b,y=b,z=b} return {x=a.x*b.x,y=a.y*b.y,z=a.z*b.z} end end function crs(a,b) return {x=a.y*b.z-a.z*b.y,y=a.z*b.x-a.x*b.z,z=a.x*b.y-a.y*b.x} end function norv(v) return scal(v,1/mag(v)) end function proj(a,b) return scal(norv(b),dot(a,norv(b))) end --or: function proj(a,b) return scal(b,dot(a,b)/mag(b)^2) end
For the longest time I thought for some reason functions couldn't return tables. Now that I know that's baloney, here are some of the most useful vector operations.
  • mag(v) returns the magnitude of v (||v||)
  • scal(v,s) returns the vector v scaled by the scalar s (v * s)
  • vadd(a,b) returns the vectors a added to b (a + b)
  • dot(a,b) returns the dot product (a * b)
  • crs(a,b) returns the cross product (a x b)
  • norv(v) returns the normal vector in the direction of v (v / ||v||)
  • proj(a,b) returns the component of a in the direction of b (b * (a * b) / ||b||^2)

Vector Transforms
localv={x=dot(globalv,locali),y=dot(globalv,localj),z=dot(globalv,localk)}
local i,j and k are the three basis unit vectors for the rotated reference object, and globalv is a vector representing the global coordinates of a point. localv will be the local coordinates of that point with reference to the rotated object.

This uses dots instead of projs, but the math is exactly the same. proj is just a dot product, scaled to be the length of v1 only, multiplied onto a unit vector in the direction of v2. This results in a vector output. Because all we care about is the magnitude (+-) of each unit vector, we can skip the whole multiplying by v2/||v2|| thing and just use the dot. A unit vector's magnitude is always one, so there's no need to scale to be the length of v1, as the length of v2 is always one.

function megaproj(v,i,j,k) return {x=dot(v,i),y=dot(v,j),z=dot(v,k)} end function megascal(v,i,j,k) return vadd(vadd(scal(i,v.x),scal(j,v.y)),scal(k,v.z)) end
megaproj will project a world coordinate onto local coordinates defined by the local unit vectors, i j and k. megascal will do the opposite, using the local i j and k vectors to extend the local coordinates of a point into global coordinates. These functions are very handy if you have a transform already defined by three unit vectors.

These functions will always work as inverses of each-other, meaning a transform onto or from any three unit vectors (provided no two are the same) can be inverted, "decoded" by the other function. This would mean they could be used similarly to the list of numbers in XML, just virtually without any real use that I can think of.

As it turns out, I accidentally came up with a rotation matrix here. Check the matrices tangent section of my eulers guide if you're interested. This function will generate the matrix i, j and k basis vectors from the physics sensor euler angles:
function ijkfromeuler(ex,ey,ez) cx=math.cos(ex) sx=math.sin(ex) cy=math.cos(ey) sy=math.sin(ey) cz=math.cos(ez) sz=math.sin(ez) i={x=cy*cz,y=cy*sz,z=-sy} j={x=sx*sy*cz-cx*sz,y=sx*sy*sz+cx*cz,z=sx*cy} k={x=cx*sy*cz+sx*sz,y=cx*sy*sz-sx*cz,z=cx*cy} return i,j,k end --or... function ijkfromeuler(ex,ey,ez) local cx,sx,cy,sy,cz,sz=math.cos(ex),math.sin(ex),math.cos(ey),math.sin(ey),math.cos(ez)math.sin(ez) local i,j,k={x=cy*cz,y=cy*sz,z=-sy},{x=sx*sy*cz-cx*sz,y=sx*sy*sz+cx*cz,z=sx*cy},{x=cx*sy*cz+sx*sz,y=cx*sy*sz-sx*cz,z=cx*cy} return i,j,k end
These vectors can then be used with megaproj() and megascal() to perform or reverse rotations.
Like Comment Subscribe
Do it or I will arot() your house
1 kommentarer
memnok999 4. juni 2024 kl. 13:06 
this is very helpful:steamhappy: