I'm writing a simple OpenGL game with Haskell. Whenever the user resizes the window I get their width and height. I need to calculate the largest width and height that fits inside their window while maintaining the W/H ratio of 1.6.
This is what I wrote. It works but I don't think it's the best way of doing it in Haskell. Can someone suggest some alternatives:
fixedRatio = 1.6
keepRatio (w,h) = head [(floor w',floor h') | w' <- [w, h*fixedRatio], h' <- [h, w/fixedRatio], w' <= w, h' <= h, w'/h' == fixedRatio ]
I'd do it with a guard (condition):
keepRatio (w,h) | w > expectedWidth = (floor expectedWidth, h)
| otherwise = (w, floor(fromIntegral w / fixedRatio))
where expectedWidth = fromIntegral h * fixedRatio
Try this:
keepRatio (w,h) = min sizeByWidth sizeByHeight
where sizeByWidth = (w, (w * 5) `div` 8)
sizeByHeight = ((h*8) `div` 5, h)
This assumes that you only need the aspect ratio to the nearest pixel.
Related
I am making a game of pong in haskell using gloss as my main library. The movement of the paddle controlled by player is tied to the x-coordinate of the mouse and is updated as it moves. The problem is, the ball doesn't register collision with the paddle while it is moving.
movePaddle :: Event -> Game -> Game
movePaddle (EventMotion (x,y)) g = g {nextPos = max 50 . min 750 $ (x+400)}
movePaddle _ g = g
movePaddle updates the next position of the player's paddle.
moveBall :: Game -> Float -> Game
moveBall g f = g {dir = dirn, ball = (xt, yt)}
Then, moveBall updates the direction in which the ball is moving, according to collision, and adjusts its coordinates.
updatePaddle :: Game -> Game
updatePaddle g = g {p1 = nextPos g}
Finally, updatePaddle updates the actual position of the paddle in the game.
My main function looks like this:
main = play
(InWindow "pong" (800, 800) (300, 300))
white
60
(Game 400 400 (400, 400) 0.72 5 400)
gameDraw
movePaddle
updatePaddle . (flip moveBall)
I have tried updating the state only as the mouse moves, but then the ball won't move. If I let it move independently while also updating it during the event, it doubles its speed during the mouse movement. What can I do to create proper collision while still being able to move the paddle?
Edit: here's my moveBall code:
moveBall :: Float -> Game -> Game
moveBall f g = g {dir = dirn, ball = (xt, yt)}
where
xn = fst (ball g) + f * vel g * cos (dir g)
yn = snd (ball g) + f * vel g * sin (dir g)
dirx
|(max 10 $ min 790 xn) == xn = 1
|otherwise = -1
diry
|(max 10 $ min 790 yn) == yn = 1
|otherwise = -1
dirn
|dirx == -1 = pi - diry * dir g
|(diry == -1) && (dirx == 1) = diry * dir g
|max (p1 g - 50) (min (p1 g + 50) xn) == xn && (yn <= 30) = paddleAngle (p1 g) xn
|max (p2 g - 50) (min (p2 g + 50) xn) == xn && (yn >= 770) = - paddleAngle (p2 g) xn
|otherwise = (dir g)
xt = (f * vel g * cos dirn) + normalize (fst (ball g))
yt = (f * vel g * sin dirn) + normalize (snd (ball g))
Solved. Turns out I was looking at the wrong paddle when registering collision and mixed up my coordinates, due to which collision for paddle 1 was being registered at the top of the screen, where paddle 2 is.
Here's a problem that's been wrecking my brain for a while.
Given:
I have two coordinate spaces:
the global space G, and
a local space A, and
I know the position and rotation of A relative to G.
Question:
How can I programmatically calculate the position and rotation of G relative to A?
On graph paper, I can calculate this by hand:
if A relative to G is (4, 1) 90deg, then G relative to A is (-1, -4) -90deg
if A relative to G is (5, 0) 0deg, then G relative to A is (-5, 0) 0deg
... but I'm having trouble transferring this calculation to software.
In matrix form,
y = R x + t
where R is the rotation matrix and t the translation of the origin.
The reverse way,
x = R' (y - t) = R' y + (- R' t)
where R' is the inverse of R, and also its transpose.
When doing a ray trace with rayTraceP, I can find the point where a ray intersects with a diagram.
> rayTraceP (p2 (0, 0)) (r2 (1, 0)) ((p2 (1,-1) ~~ p2 (1,1))
Just (p2 (1.0, 0.0))
I want to use this to find not only the "collision point", but also the collision time and the normal vector to the surface at that point.
-- A Collision has a time, a contact point, and a normal vector.
-- The normal vector is perpendicular to the surface at the contact
-- point.
data Collision v n = Collision n (Point v n) (v n)
deriving (Show)
Given a start point for the ray and a velocity vector along the ray, I can find the contact point end using rayTraceP:
end <- rayTraceP start vel dia
And I can find the collision time using the distance between start and end:
time = distance start end / norm vel
But I'm stuck on finding the normal vector. I'm working within this function:
rayTraceC :: (Metric v, OrderedField n)
=> Point v n -> v n -> QDiagram B v n Any -> Maybe (Collision v n)
-- Takes a starting position for the ray, a velocity vector for the
-- ray, and a diagram to trace the ray to. If the ray intersects with
-- the diagram, it returns a Collision containing:
-- * The amount of time it takes for a point along the ray going at
-- the given velocity to intersect with the diagram.
-- * The point at which it intersects with the diagram.
-- * The normal vector to the surface at that point (which will be
-- perpendicular to the surface there).
-- If the ray does not intersect with the diagram, it returns Nothing.
rayTraceC start vel dia =
do
end <- rayTraceP start vel dia
let time = distance start end / norm vel
-- This is where I'm getting stuck.
-- How do I find the normal vector?
let normalV = ???
return (Collision time end normalV)
Some examples of what I want it to do:
> -- colliding straight on:
> rayTraceC (p2 (0, 0)) (r2 (1, 0)) (p2 (1,-1) ~~ p2 (1,1))
Just (Collision 1 (p2 (1, 0)) (r2 (-1, 0)))
> -- colliding from a diagonal:
> rayTraceC (p2 (0, 0)) (r2 (1, 1)) (p2 (1,0) ~~ p2 (1,2))
Just (Collision 1 (p2 (1, 1)) (r2 (-1, 0))
> -- colliding onto a diagonal:
> rayTraceC (p2 (0, 0)) (r2 (1, 0)) (p2 (0,-1) ~~ p2 (2,1))
Just (Collision 1 (p2 (1, 0)) (r2 (-√2/2, √2/2)))
> -- no collision
> rayTraceC (p2 (0, 0)) (r2 (1, 0)) (p2 (1,1) ~~ p2 (1,2))
Nothing
It is correct on everything in these examples except for the normal vector.
I have looked in the documentation for both Diagrams.Trace and Diagrams.Core.Trace, but maybe I'm looking in the wrong places.
There is no way to do this in general; it depends on what exactly you hit. There is a module Diagrams.Tangent for computing tangents of trails, but to compute the tangent at a given point you have to know its parameter with respect to the trail; and one thing we are missing at the moment is a way to convert from a given point to the parameter of the closest point on a given segment/trail/path (it's been on the to-do list for a while).
Dreaming even bigger, perhaps traces themselves ought to return something more informative---not just parameters telling you how far along the ray the hit are, but also information about what you hit (from which one could more easily do things like compute a normal vector).
What kinds of things are you computing traces of? There might be a way to take advantage of the particular details of your use case to get the normals you want in a not-too-terrible way.
Brent Yorgey's answer points out the Diagrams.Tangent module, and in particular normalAtParam, which works on Parameteric functions, including trails, but not all Diagrams.
Fortunately, many 2D diagram functions, like circle, square, rect, ~~, etc. can actually return any TrailLike type, including Trail V2 n. So a function with the type
rayTraceTrailC :: forall n . (RealFloat n, Epsilon n)
=>
Point V2 n
-> V2 n
-> Located (Trail V2 n)
-> Maybe (Collision V2 n)
Would actually work on the values returned by circle, square, rect, ~~, etc. if it could be defined:
> rayTraceTrailC
(p2 (0, 0))
(r2 (1, 0))
(circle 1 # moveTo (p2 (2,0)))
Just (Collision 1 (p2 (1, 0)) (r2 (-1, 0)))
And this function can be defined by breaking the trail up into a list of fixed segments which are either linear or bezier curves, using the fixTrail function. That reduces the problem to the simpler rayTraceFixedSegmentC.
rayTraceTrailC start vel trail =
combine (mapMaybe (rayTraceFixedSegmentC start vel) (fixTrail trail))
where
combine [] = Nothing
combine cs = Just (minimumBy (\(Collision a _ _) (Collision b _ _) -> compare a b) cs)
The rayTraceFixedSegmentC can use rayTraceP to calculate the contact point, but we can't find the normal vector right away because we don't know what the parameter is at that contact point. So punt further and add fixedSegmentNormalV helper function to the wish list:
rayTraceFixedSegmentC :: forall n . (RealFloat n, Epsilon n)
=>
Point V2 n
-> V2 n
-> FixedSegment V2 n
-> Maybe (Collision V2 n)
rayTraceFixedSegmentC start vel seg =
do
end <- rayTraceP start vel (unfixTrail [seg])
let time = distance start end / norm vel
let normalV = normalize (project (fixedSegmentNormalV seg end) (negated vel))
return (Collision time end normalV)
This fixedSegmentNormalV function just has to return a normal vector for a single segment going through a single point, without worrying about the vel direction. It can destruct the FixedSegment type, and if it's linear, that's easy:
fixedSegmentNormalV :: forall n . (OrderedField n)
=>
FixedSegment V2 n -> Point V2 n -> V2 n
fixedSegmentNormalV seg pt =
case seg of
FLinear a b -> perp (b .-. a)
FCubic a b c d ->
???
In the FCubic case, to calculate the parameter where the curve goes through pt, I'm not sure what to do, but if you don't mind approximations here we can just take a bunch of points along it and find the one closest to pt. After that we can call normalAtParam as Brent Yorgey suggested.
fixedSegmentNormalV seg pt =
case seg of
FLinear a b -> perp (b .-. a)
FCubic a b c d ->
-- APPROXIMATION: find the closest parameter value t
let ts = map ((/100) . fromIntegral) [0..100]
dist t = distance (seg `atParam` t) pt
t = minimumBy (\a b -> compare (dist a) (dist b)) ts
-- once we have that parameter value we can call a built-in function
in normalAtParam seg t
With this, the rayTraceTrailC function is working with this approximation. However, it doesn't work for Diagrams, only Located Trails.
It can work on the values returned by functions like circle and rect, but not on combined diagrams. So you have to keep those building blocks of diagrams separate, as trails, for as long as you need this collision ray tracing.
Using the normal vectors to reflect the rays (the outgoing ray has an equal angle from the normal vector) looks like this:
I'm suppposed to convert a given RGB color to CMYK format, and in case of white (0,0,0) I should get (0,0,0,1). I've been trying this whole night but every time it crashes, could please someone tell what's wrong?
rgb2cmyk :: (Int,Int,Int) -> (Float,Float,Float,Float)
rgb2cmyk (r,g,b) = (c,m,y,k)
| (r,g,b) == (0,0,0) = (0,0,0,1)
| otherwise = ((w - (r/255))/w, (w - (g/255))/w, (w - (b/255))/w, 1 - w)
where
w = maximum [r/255, g/255, b/255]
I get: parse error on input '|'
You want to say either
rgb2cmyk (r, g, b) = ...
or
rgb2cymk (r, g, b)
| ... = ...
| ... = ...
But not both at the same time. (Which expression would it execute?)
As an aside, you don't actually need to test (r,g,b) == (0,0,0); you can just pattern-match (0,0,0) directly.
rgb2cymk (0,0,0) = (0,0,0,1)
rgb2cymk (r,g,b) = ???
The section = (c, m, y, k) in rgb2cmyk (r,g,b) = (c,m,y,k) is incorrect.
When using guards, you should think of it as using something like
rgb2cmyk (r,g,b) = case (r,g,b) of
(0,0,0) -> (0,0,0,1)
_ -> ...
as this is what GHC will actually rewrite your guards into (this is the same with if, as well, which turns into case predicate of...).
It doesn't make sense to write
rgb2cmyk (r,g,b) = (c,m,y,k)
And then later on have:
case (r,g,b) of ...
sitting as a floating definition in your source file.
I think I need some incentive on how to make this, I'm not really experienced in general platforming game mechanics...
Anyway, my player figure has this up to now:
movePlayer = proc p -> do
let gravity = 100
sx <- keySpeed GLFW.LEFT GLFW.RIGHT 500 -< ()
dy <- integralLim_ collision 0 -< (gravity, p)
dx <- integralLim_ collision 0 -< (sx, p)
returnA -< (sx, sy)
where
keySpeed k1 k2 s = onKey k1 (-s) <|> onKey k2 s <|> pure 0
collision = undefined -- collision with the world
With gravity, the player object slowly falls down until there is something to stand on. Of course, the next step is to add jumping, in a sin curve... what is a simple way to add it using netwire? One that can also be have further collision detecting added to it?
I just have no idea where to begin with this one.
First of all note that integrals work for tuples:
(x, y) <- integralLim_ f (x0, y0) -< ((dx, dy), w)
Now consider that gravity is an acceleration value. You can easily add it to other acceleration values:
gravity = pure (0, -9.8)
jump = pure (0, 1000) . holdFor 0.1 (keyPressed space) <|> pure (0, 0)
pos = integralLim_ collision p0 . integral_ v0 . (gravity ^+^ jump)
where p0 is the initial position and v0 the initial velocity.