Here’s some commented code.
function setup()
--[[
Define a "fishbowl", by rights I'd make this an object of a class but this is a
simple demo. We define its characteristics with hopefully obvious names.
Though "height" and "width" are actually the half-height and half-width.
--]]
fishbowl = {
width = 200,
height = 100,
centre = vec2(WIDTH/2,HEIGHT/2),
colour = color(0, 255, 186, 255)
}
--[[
Now the same for the fish. The "thrust" is how much force it can produce when
swimming. It would be possible to make this variable in some way to
correspond to the fish getting tired.
--]]
fish = {
position = vec2(WIDTH/2,HEIGHT/2),
velocity = vec2(0,0),
size = 30,
colour = color(255, 4, 0, 255),
thrust = 100,
}
-- friction is the amount of drag by the water on the fish.
friction = 1
--[[
When it bumps its head on the edge, we remember the time. If it bumps
twice in quick succession then it doesn't bounce but stays by the glass.
Fish don't really bounce when they bump against the edge of a tank, but they
do flinch and swim backwards away which is sort of like bouncing. But as it
is voluntary we assume that if the two collisions are close enough in time then
it remembers the first and doesn't flinch.
--]]
collisionTime = 0
-- This is how far apart collisions can be for the fish to not flinch.
collisionDelta = 1
end
function draw()
background(40, 40, 50)
--[[
Draw the fishbowl, making it larger by the fish's size so that when the
position of the fish is at the edge of the bowl then the whole of the fish
is still inside the bowl.
--]]
fill(fishbowl.colour)
ellipse(fishbowl.centre.x,
fishbowl.centre.y,
fishbowl.width*2+fish.size,
fishbowl.height*2+fish.size)
--[[
Now we update the fish's data. We start by computing the force on the
fish.
--]]
local force = vec2(0,0)
--[[
If the touch is active then the fish feels an attractive force towards it.
The direction of the force is the vector from its current position to the
touch point. This is given by the vector `touchpt - fish.position`.
However, that could be of arbitrary magnitude so we normalise it to
make it unit length. Then we multiply by `fish.thrust` to get it to be
the right magnitude.
--]]
if touchpt then
force = (touchpt - fish.position):normalize()*fish.thrust
end
--[[
A crude model of friction is that it is proportional to the speed of the object.
This is opposes the motion, so is ` - friction * fish.velocity`.
--]]
force = force - friction * fish.velocity
--[[
Now we update the position using the OLD velocity. For small time steps,
speed = distance/time so distance = speed * time. This is the distance
travelled in the time step so we add it to our current position.
--]]
fish.position = fish.position + DeltaTime * fish.velocity
--[[
We do the same with the velocity. We're using Newton's equation here,
F = m a, where a is the acceleration. So we should divide by the fish's
mass here, but we can just assume it to be 1 unit for simplicity.
--]]
fish.velocity = fish.velocity + DeltaTime * force
--[[
The next bit deals with the possibility that the fish has bumped against
the edge of the bowl. We start by figuring this out. The equations are
simpler if we work relative to the centre of the fishbowl so we define
a vector as the relative position of the fish to the fishbowl.
--]]
local relativept = fish.position - fishbowl.centre
--[[
As our fishbowl might be elliptical, the equations are simpler if we
scale so that the units are "fishbowl units". That is, 1 unit across is
half the fishbowl width, and 1 unit up is half the fishbowl height.
--]]
relativept.x = relativept.x/fishbowl.width
relativept.y = relativept.y/fishbowl.height
--[[
We want to know the "normal" vector to the fishbowl at the point of
collision. This is the vector that points straight out of the fishbowl.
A bit of geometry or calculus says that it is (b x/a, a y/b) where (x,y)
is the position on the fishbowl edge and (a,b) are the half-width and
half-height. As "relativept" has already been rescaled, we've already
done the divisions and just need to do the multiplications. We
renormalise to make it unit length (which makes life easier later).
--]]
local normal = vec2(relativept.x*fishbowl.height,
relativept.y*fishbowl.width):normalize()
--[[
Now we test to see if we've collided with the bowl edge. This happens
if the current position (which has been updated but not yet drawn) is
outside the fishbowl. The test for this is that the length of the relative
position vector in fishbowl-coordinates is greater than 1.
But it might be that we're outside but the collision happened last cycle.
(The amounts we move in each cycle are so small that the user won't
have noticed that we're outside.)
We detect this by looking at the direction the fish is swimming in. If it
is heading outwards then the collision hasn't happened yet so we
register it now. If it is heading inwards then the collision has already
been dealt with and we don't need to do anything.
We test for this by taking the dot product of the velocity vector and the
normal vector. The dot product can be used to test if two vectors are
in roughly the same direction or not. If it is positive then they point
roughly the same way, if negative then opposite ways.
--]]
if relativept:lenSqr() > 1 and
fish.velocity:dot(normal) > 0
then
--[[
Okay, so there was a collision. If there wasn't a lot of time since the
last collision then we just move back to the fishbowl edge.
This is not completely accurate, but future draw cycles will adjust it
so that the user can't tell that it isn't perfect.
If there was a lot of time then we "bounce". This is done by adjusting the
velocity vector so that whatever pointed out of the fishbowl is now pointing
back into it. Technically, we reflect the velocity vector in the line tangential
to the fishbowl at the point of contact. Turns out this is dot products again.
--]]
if ElapsedTime - collisionTime < collisionDelta then
fish.velocity = fish.velocity - fish.velocity:dot(normal) * normal
else
fish.velocity = fish.velocity - 2*fish.velocity:dot(normal) * normal
end
--[[
Now we store the latest collision time so that we can make the right choice
next time around.
--]]
collisionTime = ElapsedTime
end
--[[
After all that, we draw the "fish".
--]]
fill(fish.colour)
ellipse(fish.position.x,fish.position.y,fish.size)
end
function touched(t)
--[[
If a touch is active, we set "touchpt" to be its vector, otherwise to nil.
--]]
if t.state ~= ENDED then
touchpt = vec2(t.x,t.y)
else
touchpt = nil
end
end