new version:
collision fixes
ball spin now affected by paddle
paddle motion during impact can induce spin
curveballs are now possible, though practically unavoidable
#Conditional Hooks
$Application: FS2_Open
$On Game Init:
[
--HudPong2 (tm)
--------------------------------
----- init data structures -----
--------------------------------
--init bounding boxes for collision detection in local components
function initBoundingBox(w,h)
local bBox = {}
bBox.x1 = -(w/2)
bBox.x2 = w/2
bBox.y1 = -(h/2)
bBox.y2 = h/2
return bBox
end
--init player variables
function initPongPlayer(playerNum)
local player = {}
--read only
player.name = "Player "..playerNum
--writeable
player.score = 0
--input data in the range of 0 to 1
player.inputX = 0.5
player.inputY = 0.5
return player
end
--init paddle (centers at x,y, makes paddle of dimentions w,h)
function initPongPaddle(x,y,w,h)
local paddle = {}
--read only
paddle.w = w
paddle.h = h
paddle.bBoxLocal = initBoundingBox(paddle.w,paddle.h)
--writeable
paddle.xPos = x --world position
paddle.yPos = y
paddle.xPosOld = paddle.xPos --frame old values for velocity computation (to impart ball spin and/or speed)
paddle.yPosOld = paddle.yPos
paddle.xVel = 0 --velocity data goes here
paddle.yVel = 0
--translate local bbox to world
paddle.bBoxWorld = xlateBoundingBox(paddle.bBoxLocal,paddle.xPos,paddle.yPos)
return paddle
end
--init (or re-serve) the ball (centers ball at x/y, of radius r, and initial speed s)
--optional string argument d indicates serve direction: "left" or "right", if ommited direction is chosen at random
function initPongBall(x,y,r,s,d)
local ball = {}
--read only
ball.radius = r
ball.bBoxLocal = initBoundingBox(ball.radius*2,ball.radius*2)
--writeable
ball.xPos = x --ball position
ball.yPos = y
ball.aRot = 0 --used for spin animation
--translate local bbox to world
ball.bBoxWorld = xlateBoundingBox(ball.bBoxLocal,ball.xPos,ball.yPos)
--ball physics (will assume ball mass of 1kg)
ball.lVel = 0 --magnitude of linear velocity
ball.aVel = 0 --ball spin velocity (rads a sec)
--figure out direction of serve
local dir
if type(d) ~= "string" then
dir = (math.random() >= 0.5) --no direction specified so choose serve direction at random
else
dir = (d == "left") -- left is boolean true in this case
end
--calculate velocity at game start
ball.yVel = (math.random()-0.5)*s --vertical velocity in the rang of -0.5*s to 0.5*s
if dir then --left serve
ball.xVel = -s + math.abs(ball.yVel) --compensate for vertical component to always achieve a vector magnitude of 1*s
else --right serve
ball.xVel = s - math.abs(ball.yVel)
end
--misc data (mostly for debug)
ball.deltaLinVel = 0 --last change in linear/angular velocity (magnitudes only) (computed each collision)
ball.deltaAngVel = 0
ball.lift = 0 --this is our fudged lift force (acceleration because mass = 1) that makes curve balls possible
--seconds till a new ball starts moving (randomize it a little)
ball.startDelay = 0.5 + (math.random()*1.5)
--seconds after the ball "hits" the goal boundry to remove it from play
ball.killDelay = 0.5
ball.killed = false --goes true when the ball "dies"
return ball
end
--setup game to fit in xywh dimentions
function initPong(x,y,w,h)
--tableize!
local pong = {}
--gameinfo
pong.w = w --game size
pong.h = h
pong.x1 = x --game position (top left corner)
pong.y1 = y
pong.x2 = x+w --lower right corner
pong.y2 = y+h
pong.xC = x+(w/2)
pong.yC = y+(h/2)
pong.pause = false
pong.maxScore = 10 --you "win" when you get to this score first
--side indicies
pong.leftIdx = 1
pong.rightIdx = 2
--player info
pong.player = {}
pong.player[pong.leftIdx] = initPongPlayer(pong.leftIdx)
pong.player[pong.rightIdx] = initPongPlayer(pong.rightIdx)
--paddle info
pong.paddle = {}
pong.paddle[pong.leftIdx] = initPongPaddle(x + (w/256), y+(h/2), w/128, h/16)
pong.paddle[pong.rightIdx] = initPongPaddle((x+w)-(w/256), y+(h/2), w/128, h/16)
--ball info
pong.ball = initPongBall(x+(w/2), y+(h/2), ((w/128)+(h/128))/2, w/4)
--enable debug readouts
pong.debug = true
--that is all
return pong
end
--------------------------
----- draw functions -----
--------------------------
--draw funcs
function drawPongField(pong) --pong box here
--field
gr.setColor(0,50,0,20)
gr.drawRectangle(pong.x1, pong.y1, pong.x2, pong.y2, true)
--center
gr.setColor(0,100,0,40)
gr.drawCircle(pong.h/6,pong.xC,pong.yC)
gr.drawLine(pong.xC,pong.y1,pong.xC,pong.y2)
--sidelines
gr.setColor(0,200,0,255)
gr.drawLine(pong.x1, pong.y1, pong.x2, pong.y1)
gr.drawLine(pong.x1, pong.y2, pong.x2, pong.y2)
--scoreboard
gr.setColor(50,200,100,255)
local name = pong.player[pong.leftIdx].name.." "
local score = tostring(pong.player[pong.leftIdx].score).." "
gr.drawString(name.." "..pong.player[pong.rightIdx].name,pong.xC - gr.getStringWidth(name), pong.y1 + gr.CurrentFont.Height)
gr.drawString(score.." "..tostring(pong.player[pong.rightIdx].score),pong.xC - gr.getStringWidth(score), pong.y1 + (gr.CurrentFont.Height*2))
--debug
if pong.debug then
gr.setColor(255,255,255,255)
gr.drawString("Debug info:", pong.x1 + gr.CurrentFont.Height, pong.y1 + gr.CurrentFont.Height)
gr.drawString("Current linear velocity: "..tostring(pong.ball.lVel))
gr.drawString("Current angular velocity: "..tostring(pong.ball.aVel))
gr.drawString("Last linear delta velocity: "..tostring(pong.ball.deltaLinVel))
gr.drawString("Last angular delta velocity: "..tostring(pong.ball.deltaAngVel))
gr.drawString("Current lift 'force': "..tostring(pong.ball.lift))
end
end
--draw paddle
function drawPongPaddle(paddle)
gr.setColor(0,0,200,255)
gr.drawRectangle(paddle.bBoxWorld.x1, paddle.bBoxWorld.y1, paddle.bBoxWorld.x2, paddle.bBoxWorld.y2, true)
end
--this is a vector image of the radiation symbol, just to inform people who wrote this
pongBallEffect = {
{x = 0.0000, y = 0.0000},
{x = -0.500, y = 0.866},
{x = -1.000, y = 0.000},
{x = 0.0000, y = 0.0000},
{x = -0.500, y = -0.866},
{x = 0.500, y = -0.866},
{x = 0.0000, y = 0.0000},
{x = 1.000, y = 0.000},
{x = 0.500, y = 0.866},
{x = 0.0000, y = 0.0000},
}
--draw ball and related effects
function drawPongBall(ball)
gr.setColor(50,0,0,255)
gr.drawCircle(ball.radius,ball.xPos,ball.yPos)
--animate rotation effect
ball.aRot = ball.aRot + ball.aVel*ba.getFrametime()*0.1 --(ball onlu showed as spinning at 1/10th velocity)
--transform effect
local newEffect = {}
--transform
for i=1, #pongBallEffect do
newEffect[i] = {}
newEffect[i].x = pongBallEffect[i].x
newEffect[i].y = pongBallEffect[i].y
--rotate
newEffect[i].x, newEffect[i].y = rot2D(newEffect[i].x, newEffect[i].y, ball.aRot)
--scale
newEffect[i].x, newEffect[i].y = scale2D(newEffect[i].x, newEffect[i].y, ball.radius)
--translate
newEffect[i].x, newEffect[i].y = add2D(newEffect[i].x, newEffect[i].y, ball.xPos, ball.yPos)
end
--render
gr.setColor(200,0,0,255)
for i=1, #newEffect-1 do
gr.drawLine(newEffect[i].x,newEffect[i].y,newEffect[i+1].x,newEffect[i+1].y)
end
end
--draw everything
function drawPong(pong)
drawPongField(pong)
drawPongPaddle(pong.paddle[pong.leftIdx])
drawPongPaddle(pong.paddle[pong.rightIdx])
drawPongBall(pong.ball)
end
-----------------
----- maths -----
-----------------
--2d add
function add2D(x1,y1,x2,y2) return x1+x2, y1+y2 end
--2d sub
function sub2D(x1,y1,x2,y2) return x1-x2, y1-y2 end
--2d scale
function scale2D(x,y,s) return x*s, y*s end
--2d dot product
function dot2D(x1,y1,x2,y2) return x1*x2+y1*y2 end
--get prependicular vector (right angle)
function prep2D(x,y) return -y,x end
--get magnitude
function mag2D(x,y) return math.sqrt(x*x+y*y) end
--normalize (also returns magnitude as a 3rd argument)
function norm2D(x,y)
local mag = mag2D(x,y)
local magR = 1/mag
return x*magR, y*magR, mag
end
--rotate
function rot2D(x,y,r)
local s = math.sin(r)
local c = math.cos(r)
return x*c-y*s,x*s+y*c
end
--translate a bbox and returns a copy
function xlateBoundingBox(bBox,x,y)
local bBox2 = {}
--remember that tables are passed by reference, operate on components
bBox2.x1 = bBox.x1 + x
bBox2.x2 = bBox.x2 + x
bBox2.y1 = bBox.y1 + y
bBox2.y2 = bBox.y2 + y
return bBox2
end
-------------------------
----- PONG PHYSICS! -----
-------------------------
--check collision between bboxes (only used for paddle-ball collisions)
function collideBoundingBox(bBox1,bBox2)
--eliminate vertical non collision scenarios
if bBox1.y2 < bBox2.y1 or bBox1.y1 > bBox2.y2 then return false end
--eliminate horizontal non collision scenarios
if bBox1.x2 < bBox2.x1 or bBox1.x1 > bBox2.x2 then return false end
--no eliminations must have collided
return true
end
--check for collision between bounding box and the "walls" of the game (second return indicates side "top" or "bottom")
function collideBoundingBoxWall(bBox, topWallPos, bottomWallPos)
--check top wall
if bBox.y1 < topWallPos then return true, "top" end
--check bottom wall
if bBox.y2 > bottomWallPos then return true, "bottom" end
--no collision
return false
end
--check for "collision" between bounding box and the invisible goal boundry (second return indicates side "left" or "right")
function collideBoundingBoxGoal(bBox, leftGoalPos, rightGoalPos)
--check left goal
if bBox.x1 < leftGoalPos then return true, "left" end
--check right goal
if bBox.x2 > rightGoalPos then return true, "right" end
--no collision
return false
end
--move ball
function movePongBall(ball)
--[[do not work
apply "lift"
local d = 1 --fudged density value
local v = 2*math.pi*ball.radius*ball.aVel --nasa calls this rotational velocity
local g = 2*math.pi*ball.radius*v --and this is called vortex strength
local l = d*g*mag2D(ball.xVel, ball.yVel) --this is the lift force, but since mass = 1 and accel=force/mass this is also our acceleration
--]]
--normalize velocity to get flight direction (also get magnitude)
local vX,vY
vX,vY,ball.lVel = norm2D(ball.xVel, ball.yVel)
--get side vector
vX, vY = prep2D(vX, vY)
--compute "lift" "force"
ball.lift = ball.lVel*ball.aVel*ba.getFrametime()*0.1 --moar fudge factors!
--scale prependicular vector by our lift vector (adjusted for frametime)
vX, vY = scale2D(vX, vY, ball.lift*ba.getFrametime())
--apply to velocity
ball.xVel, ball.yVel = add2D(ball.xVel, ball.yVel, vX, vY )
--move ball using velocity
ball.xPos, ball.yPos = add2D(ball.xPos, ball.yPos, ball.xVel*ba.getFrametime(), ball.yVel*ba.getFrametime())
--update world bounding box
ball.bBoxWorld = xlateBoundingBox(ball.bBoxLocal,ball.xPos,ball.yPos)
end
--move paddle (also computes velocity)
function movePongPaddle(paddle, xPos, yPos)
--store old data
paddle.xPosOld = paddle.xPos
paddle.yPosOld = paddle.yPos
--set paddle to new position
paddle.xPos = xPos
paddle.yPos = yPos
--compute velocity
paddle.xVel = (paddle.xPos - paddle.xPosOld)*ba.getFrametime()*60 --yet another fudge factor!
paddle.yVel = (paddle.yPos - paddle.yPosOld) *ba.getFrametime()*60 --make it a little bit faster but still comp for variable framerate
--recompute bounding box
paddle.bBoxWorld = xlateBoundingBox(paddle.bBoxLocal,paddle.xPos,paddle.yPos)
end
---------------
---- pong -----
---------------
--do input for players, takes pong game, input value for p1 0 to 1, input value for p2 0 to 1
function doPongInput(pong,lInputX,lInputY,rInputX,rInputY)
--qualify input (must be in the range 0-1)
if lInputX < 0 then lInputX = 0 end
if lInputX > 1 then lInputX = 1 end
if lInputY < 0 then lInputY = 0 end
if lInputY > 1 then lInputY = 1 end
if rInputX < 0 then rInputX = 0 end
if rInputX > 1 then rInputX = 1 end
if rInputY < 0 then rInputY = 0 end
if rInputY > 1 then rInputY = 1 end
--post input
pong.player[pong.leftIdx].inputX = lInputX
pong.player[pong.leftIdx].inputY = lInputY
pong.player[pong.rightIdx].inputX = rInputX
pong.player[pong.rightIdx].inputY = rInputY
end
function doPongFrame(pong)
--dont do anything if the game is paused
if not pong.pause then
--move paddles according to input for each player
for i=1, #pong.player do
--keep the paddles between the lines
local newPosX = pong.paddle[i].xPos
--[[expiremental 2 axis mode
if i==pong.leftIdx then
newPosX = pong.x1 + (pong.paddle[i].w/2) + (pong.player[i].inputX * (pong.w/4))
else
newPosX = ((pong.x1 + (pong.w*0.75)) - (pong.paddle[i].w/2)) + (pong.player[i].inputX * (pong.w/4))
end
]]--
local newPosY = (pong.y1+1+(pong.paddle[i].h/2)) + ((pong.h-pong.paddle[i].h)*pong.player[i].inputY)
--move that sucker
movePongPaddle(pong.paddle[i], newPosX, newPosY)
end
--maybe move ball (this is easy)
if pong.ball.startDelay <= 0 then
movePongBall(pong.ball)
else --otherwise countdown till start
pong.ball.startDelay = pong.ball.startDelay - ba.getFrametime()
end
--check to see if ball is dead
if pong.ball.killed then
pong.ball.killDelay = pong.ball.killDelay - ba.getFrametime()
--maybe re-init the ball
if pong.ball.killDelay <= 0 then
pong.ball = initPongBall(pong.x1+(pong.w/2), pong.y1+(pong.h/2), ((pong.w/128)+(pong.h/128))/2, pong.w/4)
end
else --check for collisions and respond appropriately
--ball-wall collisions
local hit, dir = collideBoundingBoxWall(pong.ball.bBoxWorld,pong.y1,pong.y2)
if hit then --do collision response
--face normals for our sideline
local fVecX,fVecY
if dir == "top" then
fVecX, fVecY = 0,1 --front normal
else
fVecX, fVecY = 0,-1
end
--do collision response only if the ball is going at the wall when the collision takes place
if dot2D(fVecX,fVecY,pong.ball.xVel,pong.ball.yVel) < 0 then
local velX,velY,velA
--this is a fudged zero entropy deflection
velY = -pong.ball.yVel
--calculate spin (multiply by radius to calculate torque, since mass (and likewise moi) = 1, torque = angular acceleration)
velA = pong.ball.radius*pong.ball.xVel*-fVecY
--calculate change in velocity caused by ball rotation during impact
pong.ball.deltaLinVel = -(pong.ball.aVel/pong.ball.radius)*fVecY
velX = pong.ball.xVel + pong.ball.deltaLinVel
velA = velA - pong.ball.deltaLinVel
--apply new physics
pong.ball.xVel = velX
pong.ball.yVel = velY
pong.ball.deltaAngVel = (velA*ba.getFrametime())
pong.ball.aVel = pong.ball.aVel + pong.ball.deltaAngVel
end
end
--ball-paddle collisions
for i=1, #pong.paddle do --do for each paddle
hit = collideBoundingBox(pong.ball.bBoxWorld,pong.paddle[i].bBoxWorld)
if hit then --do collision response
--since both ball and paddle can have velocity, calculate paddel-local velocity for ball
local lpvX,lpvY = sub2D(pong.ball.xVel, pong.ball.yVel, pong.paddle[i].xVel, pong.paddle[i].yVel)
--side and face normals for our paddles
local fVecX,fVecY
if i == pong.leftIdx then
fVecX,fVecY = 1,0
else
fVecX,fVecY = -1,0
end
--do collision response only if the ball is going at the paddle when the collision takes place
if dot2D(fVecX,fVecY,lpvX,lpvY) < 0 then
local velX,velY,velA
--this is a fudged zero entropy deflection
velX = -lpvX
--calculate spin (multiply by radius to calculate torque, since mass (and likewise moi) = 1, torque = angular acceleration)
velA = pong.ball.radius*lpvY*fVecX
--calculate change in velocity caused by ball rotation during impact
pong.ball.deltaLinVel = -(pong.ball.aVel/pong.ball.radius)*-fVecX
velY = lpvY + pong.ball.deltaLinVel
velA = velA - pong.ball.deltaLinVel
--apply new physics
pong.ball.xVel = velX
pong.ball.yVel = velY
pong.ball.deltaAngVel = (velA*ba.getFrametime())
pong.ball.aVel = pong.ball.aVel + pong.ball.deltaAngVel
break --we can only hit one paddle at a time
end
end
end
--this is mainly only used for scoring so it will be fairly easy
hit, dir = collideBoundingBoxGoal(pong.ball.bBoxWorld,pong.x1,pong.x2)
if hit then
--face normals for our goals and who gets the point should a collision occure
local fVecX,fVecY,sIdx
if dir == "left" then --score for player 2 (right player)
fVecX,fVecY,sIdx = 1,0,pong.rightIdx
else --score for player 1 (left player)
fVecX,fVecY,sIdx = -1,0,pong.leftIdx
end
--do collision response only if the ball is going twards the goal (it may have been deflected by the paddle this frame)
if dot2D(fVecX,fVecY,pong.ball.xVel,pong.ball.yVel) < 0 then
--score goal
pong.player[sIdx].score = pong.player[sIdx].score + 1
--kill the ball
pong.ball.killed = true
end
end
end
end
--draw it
drawPong(pong)
end
---------------------
----- rtt Pong! -----
---------------------
--works the same as initPong(), except x and y are considered 0, and returns a texture handle as a second return value
function initRttPong(w,h)
local pong = initPong(0,0,w,h)
local pongTex = gr.createTexture(w,h,TEXTURE_DYNAMIC)
return pong, pongTex
end
--works about the same as doPongFrame(), except requires a texture handle to the rtt texture (also returns said handle)
function doRttPongFrame(pong, pongTex)
local oldtarget = gr.CurrentRenderTarget --save the old target while we work
gr.CurrentRenderTarget = pongTex --set new render target
doPongFrame(pong) --do the pong
gr.CurrentRenderTarget = oldtarget --go back to the previous target
return pongTex
end
]
; lab pong
$State: GS_STATE_LAB ;lab pong
$On State Start:
[ labPong = initPong(100,100,gr.getScreenWidth()-200,gr.getScreenHeight()-200) ]
$On Frame:
[
--drive input
doPongInput(labPong, io.getMouseX()/gr.getScreenWidth(), io.getMouseY()/gr.getScreenHeight(), io.getMouseX()/gr.getScreenWidth(), io.getMouseY()/gr.getScreenHeight())
--run it
doPongFrame(labPong)
]
$On State End:
[ labPong = nil ]
; rtt pong
$State: GS_STATE_GAME_PLAY
$On State Start:
[ rttPong,tex = initRttPong(512,256) ]
$On Frame:
[
--drive input
doPongInput(rttPong, io.getMouseX()/gr.getScreenWidth(), io.getMouseY()/gr.getScreenHeight(), io.getMouseX()/gr.getScreenWidth(), io.getMouseY()/gr.getScreenHeight())
--run it
tex = doRttPongFrame(rttPong,tex)
--replace ship textures
ship = mn.Ships["Alpha 1"]
if ship:isValid() then
for i=1, #ship.Textures do
ship.Textures[i] = tex
end
end
]
$On State End:
[
rttPong = nil
tex:unload()
]
#End