Related
I am trying to make a Wolfenstein3D-like game, using pico-8 (that's a 2d engine with many limitations) and the world just bends very weirdly.
Gif of running around
My code: (Warning LUA! Confusing language. starts counting at 1...! )
function ray_cast()
points = {}
for i=1,64 do
points[i] = -1
end
for o = -31,32 do
local angle = player.lvec - o/256
for i=0,96 do
local x,y
x = i * cos(angle)
y = i * sin(angle)
if mget((x+player.x)/8,(y+player.y)/8) == 1 then
local tx = i-1 * cos(angle)
local ty = i-1 * sin(angle)
local dis = sqrt((tx^2+ty^2))
points[o+32] = dis*cos(angle-player.lvec)
break
end
end
end
end
I asked for help in the PICO-8 discord and someone said they could help me, but after a lot of messaging, it still did not result in the solution I wanted.
Edit: New GIF
Distortion of things
I don't know the language of Lua, however, I do understand the basics of ray-casting. From what I see, the issue could be a too large FOV size or the angles not being fixed (subtracting 360 degrees/2PI radians when above 360 degrees/2PI radians, or adding 360 degrees/2PI radians when below 0). If you don't "fix" the angles, you may get incorrect ray hit positions or even in some cases, crash the GUI. If you have a large FOV, the world will appear to be warped.
FOV OF 64 DEGREES:
FOV OF 128 DEGREES:
I recently made a simple lua class (this is using the love2D engine) that made a hexagon by plotting 6 vertices and tracing them with lines, one of the things I wanted to be able to expand to was a hexagon grid but I didn't know how
All of the websites I found didn't help and I couldn't understand why.
Here's the function where I plot the vertices based on it's radius and x and y position.
My hexagons are in a pointy-topped style.
function hexagon.new(x,y,radius)
local hexagon=setmetatable({},hexagon)
hexagon.Vertices={}
hexagon.x=x or 0
hexagon.radius = radius or 10
hexagon.y=y or 0
for i=0,6 do
local angle = 2 * math.pi / 6 * (i + .5) -- 1 is what is multipled to 90(2*math.pi) so 1*90=90(flat-topped), 0.5*90=45(pointy-topped)
local x = hexagon.x + hexagon.radius * math.cos(angle)
local y = hexagon.y + hexagon.radius * math.sin(angle)
hexagon.Vertices[i]= {x=x,y=y}
end
return hexagon
end
Hope i understood well.
Here is working example of how to create hex and then draw it to grid with as simplified functions as possible, using Love2d engine (v. 0.10.2).
important: made for hex which has all six points on circle, using radius.
this approach is not suitable for all hex shapes.
------------------------------
--lets create hex data with center at position 0,0
--using only simple functions
--table of 6 points
hex = {};
--radius for hex which has all 6 points lying on circle
hex.radius = 10;
--simple loop to generate hex
local i = 1
repeat
--full circle has 2 radians and hex has 6 points
--so 1/3 pi increase per point
local direction = math.pi/3 * (i+0.5)
--rotate hex by 90 degrees: direction = math.pi/3 * i
--generate empty table to insert point coordinations to
hex[i] = {};
--set x,y coordinates
hex[i].x = hex.radius*math.cos( direction )
hex[i].y = hex.radius*math.sin( direction )
i = i+1
until i > 6
------------------------------
------------------------------
function love.draw( dt )
--get distance between 2 hex tiles using trigonometry
local jxOffset = hex.radius*-math.tan( math.pi/1.5 ) --or math.sqrt(3) * hex.radius
--offset new lines by this value
local ixOffset = jxOffset/4
--use direction and distance to get "y" offset between lines
--we got distance in jxOffset, so we just apply direction
local iyOffset = jxOffset*math.sin( math.pi/3 )
--"i" = line, "j" = tile in line
local i = 1
repeat
local j = 1
repeat
love.graphics.push()
--offset drawable position (or draw hex tile at position):
love.graphics.translate( ixOffset+j*jxOffset, i*iyOffset )
--draw poly-line between all hex points - creating hex
love.graphics.line( hex[1].x,hex[1].y, hex[2].x,hex[2].y, hex[3].x,hex[3].y, hex[4].x,hex[4].y, hex[5].x,hex[5].y, hex[6].x,hex[6].y, hex[1].x,hex[1].y );
love.graphics.pop()
j = j+1
until j > 5
--invert for each new line
--keep lines from increasing x coordinates
ixOffset = -ixOffset
i = i+1
until i > 5
end
------------------------------
Excuse me if i made mistake somewhere, ran the code and worked properly on my side. If you discover anything off, comment and i will try to fix it.
I'd need some help from an "iso guru". I am fiddling on a game where there are two cannons placed on an isometric grid. When one cannon fires a bullet, it should fly in a curved trajectory, like shown below. While this would be an easy task on an x/y plane, I have no clue how to calculate a curved path (with variable height) on an isometric plane.
Could someone point me into the right direction please? I'd need to fire bullets from one field to any given other, while the bullets' flying altitude (the "strength" of the curve) depends on the given shot power.
Any hints? :(
Image: http://postimg.org/image/6lcqnwcrr/
This may help. The trajectory function takes some trajectory parameters (velocity, elevation, starting position and gravity) and returns a function that calcs the y position from the x position in world space.
The converter returns a function that converts between world and screen co-ords for a given projection angle.
What follows is an example of it being used to calculate the trajectory for some points in screen space.
It's really for indication purposes. It has a bunch of potential divide by zeroes but it generates trajectories that look ok for sensible elevations, projections and velocities.
-- A trajectory in world space
function trajectory(v,elevation,x0,y0,g)
x0 = x0 or 0
y0 = y0 or 0
local th = math.rad(elevation or 45)
g = g or 9.81
return function(x)
x = x-x0
local a = x*math.tan(th)
local b = (g*x^2)/(2*(v*math.cos(th))^2)
return y0+a-b
end
end
-- convert between screen and world
function converter(iso)
iso = math.rad(iso or 0)
return function(toscreen,x,y)
if toscreen then
y = y+x*math.sin(iso)
x = x*math.cos(iso)
else
x = x/math.cos(iso)
y = y-x*math.sin(iso)
end
return x,y
end
end
-- velocity 60m/s at an angle of 70 deg
t = trajectory(60,70,0,0)
-- iso projection of 30 deg
c = converter(30)
-- x in screen co-ords
for x = 0,255 do
local xx = c(false,x,0) -- x in world co-ords
local y = t(xx) -- y in world co-ords
local _,yy = c(true,xx,y) -- y in screen co-ords
local _,y0 = c(true,xx,0) --ground in screen co-ords
yy = math.floor(yy) -- not needed
if yy>y0 then print(x,yy) end -- if it's above ground
end
If there are no lateral forces you can use the 2D equation for ballistic motion in XZ plane (so y=0 at all time), then rotate via a 3D transformation around z axis to account for actual orientation of canon in 3D space. This transformation matrix is very simple, you can unfold the multiplication (write out the terms multiplied) to get the 3D equations.
Suppose I have the coordinates X, Y, Z and orientation Rx, Ry, Rz of an object with respect to a camera.
In addition, I have the coordinates U, V, W and orientation Ru, Rv, Rw of this camera in the world.
How do I transform the position (location and rotation) of the object to its position in the world?
It sounds like a change of basis to me, but I haven't found a clear source yet.
I have found this document which is quite clear on the topic.
http://www.cse.psu.edu/~rcollins/CSE486/lecture12.pdf
It treats, among others, the reverse operation, i.e., going from world to camera 3D coordinates.
Pc = R ( Pw - C )
Where, Pc is a point in the camera world, Pw is a point in the normal world, R is a rotation matrix and C is the camera translation.
Unfortunately it is rather cumbersome to add latex formulae, so I will give some matlab code instead.
function lecture12_collins()
% for plotting simplicity I choose my points on plane z=0 in this example
% Point in the world
Pw = [2 2.5 0 1]';
% rotation
th = pi/3;
% translation
c = [1 2.5 0]';
% obtain world to camera coordinate matrix
T = GetT(th, c);
% calculate the camera coordinate
Pc = T*Pw
% get the camera to world coordinate
T_ = GetT_(th, c)
% Alternatively you could use the inverse matrix
% T_ = inv(R*C)
% calculate the worldcoordinate
Pw_ = T_*Pc
assert (all(eq(Pw_ ,Pw)))
function T = GetT(th, c)
% I have assumed rotation around the z axis only here.
R = [cos(th) -sin(th) 0 0
sin(th) cos(th) 0 0
0 0 1 0
0 0 0 1];
C = [1 0 0 -c(1)
0 1 0 -c(2)
0 0 1 -c(3)
0 0 0 1];
T = R*C;
function T_ = GetT_(th, c)
% negate the angle
R_ = [cos(-th) -sin(-th) 0 0
sin(-th) cos(-th) 0 0
0 0 1 0
0 0 0 1];
% negate the translation
C_ = [1 0 0 c(1)
0 1 0 c(2)
0 0 1 c(3)
0 0 0 1];
T_ = C_*R_
So far this is about the location only. The rotation I've solved by using extra knowledge I had of the rotation. I know that my camera is perpendicular to the object and that its rotation is only around the z axis. I can just add the rotation of the camera and the object.
In fact you have two basis : one relative to the camera, the other is absolute (the world). So you basically want to transform your relative data into absolute data.
Location
This is the easiest one. You have to translate the (X,Y,Z) position by the vector t(U,V,W). So all your positions in absolute are (Ax, Ay, Az) = (X,Y,Z)+t = (X+U,Y+V,Z+W).
Orientation
This is a bit more difficult. You have to find the rotation matrix that rotate your camera from (I assume) (0,0,1) to (Ru,Rv,Rw). You should look at Basic Rotation Matrices in order to decompose the 2 rotations that take (0,0,1) to (Ru,Rv,Rw) (one according to X axis, one according to Z axis for example). I advise you to draw the absolute basis and the vector (Ru,Rv,Rw) on a sheet of paper, it is the simplest way to get the right result.
So you have 2 basic rotations matrices r1 and r2. The resultant rotation matrix r = r1*r2 (or r2*r1, it doesn't matter). So the absolute orientation of your object is (ARx, ARy, ARz)
= r*(Rx,Ry,Rz).
Hope this helps !
This is a problem I hit when trying to implement a game using the LÖVE engine, which covers box2d with Lua scripting.
The objective is simple: A turret-like object (seen from the top, on a 2D environment) needs to orientate itself so it points to a target.
The turret is on the x,y coordinates, and the target is on tx, ty. We can consider that x,y are fixed, but tx, ty tend to vary from one instant to the other (i.e. they would be the mouse cursor).
The turret has a rotor that can apply a rotational force (torque) on any given moment, clockwise or counter-clockwise. The magnitude of that force has an upper limit called maxTorque.
The turret also has certain rotational inertia, which acts for angular movement the same way mass acts for linear movement. There's no friction of any kind, so the turret will keep spinning if it has an angular velocity.
The turret has a small AI function that re-evaluates its orientation to verify that it points to the right direction, and activates the rotator. This happens every dt (~60 times per second). It looks like this right now:
function Turret:update(dt)
local x,y = self:getPositon()
local tx,ty = self:getTarget()
local maxTorque = self:getMaxTorque() -- max force of the turret rotor
local inertia = self:getInertia() -- the rotational inertia
local w = self:getAngularVelocity() -- current angular velocity of the turret
local angle = self:getAngle() -- the angle the turret is facing currently
-- the angle of the like that links the turret center with the target
local targetAngle = math.atan2(oy-y,ox-x)
local differenceAngle = _normalizeAngle(targetAngle - angle)
if(differenceAngle <= math.pi) then -- counter-clockwise is the shortest path
self:applyTorque(maxTorque)
else -- clockwise is the shortest path
self:applyTorque(-maxTorque)
end
end
... it fails. Let me explain with two illustrative situations:
The turret "oscillates" around the targetAngle.
If the target is "right behind the turret, just a little clock-wise", the turret will start applying clockwise torques, and keep applying them until the instant in which it surpasses the target angle. At that moment it will start applying torques on the opposite direction. But it will have gained a significant angular velocity, so it will keep going clockwise for some time... until the target will be "just behind, but a bit counter-clockwise". And it will start again. So the turret will oscillate or even go in round circles.
I think that my turret should start applying torques in the "opposite direction of the shortest path" before it reaches the target angle (like a car braking before stopping).
Intuitively, I think the turret should "start applying torques on the opposite direction of the shortest path when it is about half-way to the target objective". My intuition tells me that it has something to do with the angular velocity. And then there's the fact that the target is mobile - I don't know if I should take that into account somehow or just ignore it.
How do I calculate when the turret must "start braking"?
Think backwards. The turret must "start braking" when it has just enough room to decelerate from its current angular velocity to a dead stop, which is the same as the room it would need to accelerate from a dead stop to its current angular velocity, which is
|differenceAngle| = w^2*Inertia/2*MaxTorque.
You may also have some trouble with small oscillations around the target if your step time is too large; that'll require a little more finesse, you'll have to brake a little sooner, and more gently. Don't worry about that until you see it.
That should be good enough for now, but there's another catch that may trip you up later: deciding which way to go. Sometimes going the long way around is quicker, if you're going that way already. In that case you have to decide which way takes less time, which is not difficult, but again, cross that bridge when you come to it.
EDIT:
My equation was wrong, it should be Inertia/2*maxTorque, not 2*maxTorque/Inertia (that's what I get for trying to do algebra at the keyboard). I've fixed it.
Try this:
local torque = maxTorque;
if(differenceAngle > math.pi) then -- clockwise is the shortest path
torque = -torque;
end
if(differenceAngle < w*w*Inertia/(2*MaxTorque)) then -- brake
torque = -torque;
end
self:applyTorque(torque)
This seems like a problem that can be solved with a PID controller. I use them in my work to control a heater output to set a temperature.
For the 'P' component, you apply a torque that is proportional to the difference between the turret angle and the target angle i.e.
P = P0 * differenceAngle
If this still oscillates too much (it will a bit) then add an 'I' component,
integAngle = integAngle + differenceAngle * dt
I = I0 * integAngle
If this overshoots too much then add a 'D' term
derivAngle = (prevDifferenceAngle - differenceAngle) / dt
prevDifferenceAngle = differenceAngle
D = D0 * derivAngle
P0, I0 and D0 are constants that you can tune to get the behaviour that you want (i.e. how fast the turrets respond etc.)
Just as a tip, normally P0 > I0 > D0
Use these terms to determine how much torque is applied i.e.
magnitudeAngMomentum = P + I + D
EDIT:
Here is an application written using Processing that uses PID. It actually works fine without I or D. See it working here
// Demonstration of the use of PID algorithm to
// simulate a turret finding a target. The mouse pointer is the target
float dt = 1e-2;
float turretAngle = 0.0;
float turretMass = 1;
// Tune these to get different turret behaviour
float P0 = 5.0;
float I0 = 0.0;
float D0 = 0.0;
float maxAngMomentum = 1.0;
void setup() {
size(500, 500);
frameRate(1/dt);
}
void draw() {
background(0);
translate(width/2, height/2);
float angVel, angMomentum, P, I, D, diffAngle, derivDiffAngle;
float prevDiffAngle = 0.0;
float integDiffAngle = 0.0;
// Find the target
float targetX = mouseX;
float targetY = mouseY;
float targetAngle = atan2(targetY - 250, targetX - 250);
diffAngle = targetAngle - turretAngle;
integDiffAngle = integDiffAngle + diffAngle * dt;
derivDiffAngle = (prevDiffAngle - diffAngle) / dt;
P = P0 * diffAngle;
I = I0 * integDiffAngle;
D = D0 * derivDiffAngle;
angMomentum = P + I + D;
// This is the 'maxTorque' equivelant
angMomentum = constrain(angMomentum, -maxAngMomentum, maxAngMomentum);
// Ang. Momentum = mass * ang. velocity
// ang. velocity = ang. momentum / mass
angVel = angMomentum / turretMass;
turretAngle = turretAngle + angVel * dt;
// Draw the 'turret'
rotate(turretAngle);
triangle(-20, 10, -20, -10, 20, 0);
prevDiffAngle = diffAngle;
}
Ok I believe I got the solution.
This is based on Beta's idea, but with some necessary tweaks. Here it goes:
local twoPi = 2.0 * math.pi -- small optimisation
-- returns -1, 1 or 0 depending on whether x>0, x<0 or x=0
function _sign(x)
return x>0 and 1 or x<0 and -1 or 0
end
-- transforms any angle so it is on the 0-2Pi range
local _normalizeAngle = function(angle)
angle = angle % twoPi
return (angle < 0 and (angle + twoPi) or angle)
end
function Turret:update(dt)
local tx, ty = self:getTargetPosition()
local x, y = self:getPosition()
local angle = self:getAngle()
local maxTorque = self:getMaxTorque()
local inertia = self:getInertia()
local w = self:getAngularVelocity()
local targetAngle = math.atan2(ty-y,tx-x)
-- distance I have to cover
local differenceAngle = _normalizeAngle(targetAngle - angle)
-- distance it will take me to stop
local brakingAngle = _normalizeAngle(_sign(w)*2.0*w*w*inertia/maxTorque)
local torque = maxTorque
-- two of these 3 conditions must be true
local a,b,c = differenceAngle > math.pi, brakingAngle > differenceAngle, w > 0
if( (a and b) or (a and c) or (b and c) ) then
torque = -torque
end
self:applyTorque(torque)
end
The concept behind this is simple: I need to calculate how much "space" (angle) the turret needs in order to stop completely. That depends on how fast the turret moves and how much torque can it apply to itself. In a nutshell, that's what I calculate with brakingAngle.
My formula for calculating this angle is slightly different from Beta's. A friend of mine helped me out with the physics, and well, they seem to be working. Adding the sign of w was my idea.
I had to implement a "normalizing" function, which puts any angle back to the 0-2Pi zone.
Initially this was an entangled if-else-if-else. Since the conditions where very repetitive, I used some boolean logic in order to simplify the algorithm. The downside is that, even if it works ok and it is not complicated, it doesn't transpire why it works.
Once the code is a little bit more depurated I'll post a link to a demo here.
Thanks a lot.
EDIT: Working LÖVE sample is now available here. The important stuff is inside actors/AI.lua (the .love file can be opened with a zip uncompressor)
You could find an equation for angular velocity vs angular distance for the rotor when accelerating torque is applied, and find the same equation for when the braking torque is applied.
Then modify the breaking equation such that it intesects the angular distance axis at the required angle. With these two equations you can calculate the angular distance at which they intersect which would give you the breaking point.
Could be totally wrong though, not done any like this for a long time. Probably a simpler solution. I'm assuming that acceleration is not linear.
A simplified version of this problem is pretty simple to solve. Assume the motor has infinite torque, ie it can change velocity instantaneously. This is obviously not physically accurate but makes the problem much simpler to solve and in the end isn't a problem.
Focus on a target angular velocity not a target angle.
current_angle = "the turrets current angle";
target_angle = "the angle the turret should be pointing";
dt = "the timestep used for Box2D, usually 1/60";
max_omega = "the maximum speed a turret can rotate";
theta_delta = target_angle - current_angle;
normalized_delta = normalize theta_delta between -pi and pi;
delta_omega = normalized_deta / dt;
normalized_delta_omega = min( delta_omega, max_omega );
turret.SetAngularVelocity( normalized_delta_omega );
The reason this works is the turret automatically tries to move slower as it reaches its target angle.
The infinite torque is masked by the fact that the turret doesn't try to close the distance instantaneously. Instead it tries to close the distance in one timestep. Also since the range of -pi to pi is pretty small the possibly insane accelerations never show themselves. The maximum angular velocity keep the turret's rotations looking realistic.
I've never worked out the real equation for solving with torque instead of angular velocity, but I imagine it will look a lot like the PID equations.