Bowling outdated
From Blue Mars Developer Guidebook
|
|
Contents |
Overview
The Bowling minigame (Game/Extensions/BowlingExtension.xml) demonstrates the following features:
Here's the state transition diagram for the bowling game with the following key features:
- An initial state
- A finish state
- An outermost state loop that indicates we have the option of playing the game again and again until we quit.
- An inner gameplay loop representing prepare-throw-reset.
Class Description
Here's the header of our script, Game/Scripts/Entities/Minigames/AR/bowling.lua, the entity class description including the list of state names from our state transition diagram, and our entity variables.
BowlingGame =
{
Properties =
{
camJumpRight = {x=.25,y=0,z=0},
defaultImpPow = 50,
xAngImpMul = -1,
},
States =
{
"Init",
"ShowAlley",
"PrepareToThrow",
"ThrowBall",
"ResetAfterThrow",
"ClearLane",
"Finish",
},
Editor=
{
Icon="ar_test.bmp",--(in Editor/ObjectIcons)
ShowBounds=0,
},
FSCommands =
{
throw = "Bowling:Throw",
timedOut = "Bowling:TimedOut",
playagain = "Bowling:PlayAgain",
exit = "Bowling:Exit",--placeholder
},
Actions =
{
mouseX = "hud_mousex",
mouseY = "hud_mousey",
mouseClick = "hud_mouseclick",
rightMouseClickDown = "hud_mouserightbtndown",
rightMouseClickUp = "hud_mouserightbtnup",
arrowRight = "next_spectator_target",
arrowLeft = "prev_spectator_target",
},
pins={}, --for tracking pins
balls={}, --for tracking ball
frameScores={}, --cumulative score array (1 score per frame)
temp={}, --frameScore storage for debug
hud_controller = "Libs/UI/AR_Bowling/bowlCtrlContainer.swf",
hud_scoreboard = "Libs/UI/AR_Bowling/scoreboard1.swf",
playagain = "Libs/UI/AR_Bowling/playAgain.swf",
ROLLING_FIRST_BALL = 1, --scoring states
ROLLING_SECOND_BALL = 2,
STRIKE_LAST_BALL = 3,
TWO_CONSEC_STRIKES = 4,
STRIKE_2_BALLS_AGO = 5,
SPARE_LAST_BALL = 6,
rotLeftLimit = 0.04,
rotRightLimit = -0.06,
ball,
bScoreWasRecorded = false,
bPinsShouldReset = false,
bGameIsOver = false,
bRollingBackwards,
scoreState = 1,
firstBallInFrame = 0,
rollingFrame = 1,
totalScore = 0,
ballCount = 0,
time = 0,
vecX = 0,
vecY = 0,
impulsePow = 0,
lastDist = 0,
spinStrength = 0,
spinImp = 0,
breakPtDist = 8,
finalFrameNumber = 10,--number of frames in game
cam,
mXorig = 0, --initial mouse right-click x-value for aim
vInitBallPos = {x=0,y=0,z=0},
vInitCamPos = {x=0,y=0,z=0}, --centered throwing position
vInitCamAng = {x=0,y=0,z=0}, --centered aiming angle
vFirstCamAng = {x=0,y=0,z=0},--aiming angle, when right-click down
vCamPos = {x=0,y=0,z=0}, --throwing position
}
Initialization
As described in Minigame Basics, by convention we implement a Start function that initializes and launches the game. This Start function:
- initialize the entity variables that refer to the bowling ball and pins and custom camera
- load all the Flash movies used in the HUD
- set the field-of-view of the custom camera
- activate the custom camera
- enable calls to the OnUpdate callbacks in this script
- transition to the Init state
Note that commented out call to ARDebug Utilities function. That is left uncommented during development to enable verbose logging output and error-checking with the Lua debugger, and commented out when delivered.
function BowlingGame:Start()
self:GetEntities();
self:GetInitPos();
self:LoadFlash();
local bowlFov = g_Deg2Rad * 50;
Game.SetViewFov(self.cam.view, bowlFov);
self.cam:Start();
self:Activate(1);
--ARDebug(3);
self:GotoState("Init");
end
function BowlingGame:GetEntities()
self.pins = {};
i=0;
local link=self:GetLinkTarget("pin", i);
while (link) do
table.insert(self.pins, {
entity=link,
pos=link:GetWorldPos(),
ang=link:GetWorldAngles(),
scale=link:GetScale(),
});
i=i+1;
link=self:GetLinkTarget("pin", i);
end
local i=0;
local link=self:GetLinkTarget("ball", i);
while (link) do
table.insert(self.balls, {
entity=link,
pos=link:GetWorldPos(),
ang=link:GetWorldAngles(),
scale=link:GetScale(),
});
i=i+1;
link=self:GetLinkTarget("ball", i);
end
self.ball = System.GetEntityByName("Ball1");
self.cam = System.GetEntityByName("MiniCamera_1stPersonBowl");
end
function BowlingGame:GetInitPos() self.vInitBallPos = self.ball:GetWorldPos(); self.vInitCamPos = self.cam:GetWorldPos(); self.vInitCamAng = self.cam:GetWorldAngles(); end
function BowlingGame:ResetInitPos() self.bPinsShouldReset = false; --to determine whether to reset self.bScoreWasRecorded = false; self.scoreState = self.ROLLING_FIRST_BALL; self.firstBallInFrame = 0; --ball 1 score in a frame (if not a strike) self.rollingFrame = 1; --number of frame in which next ball will be rolled self.totalScore = 0; self.bGameIsOver = false; end
function BowlingGame:LoadFlash() HUD.LoadFlash(self.hud_controller); HUD.LoadFlash(self.playagain); self:LoadScoreboard(); end
function BowlingGame:LoadScoreboard() HUD.LoadFlash(self.hud_scoreboard); HUD.DockFlash(self.hud_scoreboard, eFD_Stretch); HUD.ShowFlash(self.hud_scoreboard); HUD.SetFlashVariable(self.hud_scoreboard,"totalScore"," ") end
States
Init
The initial state:
- sets the ball in the initial position
- loads the scoreboard HUD
- transitions to the PrepareToThrow state
BowlingGame.Init =
{
OnBeginState = function(self)
self:ResetInitPos();
self:LoadScoreboard();
self:GotoState("PrepareToThrow");
end,
}
PrepareToThrow
On entry, this state:
- sets the initial camera position and direction
- displays the bowling throw controller
The throw controller is a Flash movie with Actionscript that tracks the mouse and calculates throw parameters to apply to the bowling ball. The OnUpdate callback checks if the Actionscript in the throw controller has issued a throw command - if so, then it transitions to the Throw state.
BowlingGame.PrepareToThrow =
{
OnBeginState = function(self)
self.cam:SetWorldPos(self.vInitCamPos);
self.cam:SetWorldAngles(self.vInitCamAng);
CopyVector(self.vCamPos, self.vInitCamPos);
self.bAimEnabled = false;
self:ShowController();
end,
OnUpdate = function(self,time)
local action = HUD.GetLastAction();
if (action ~= "") then
if (action == self.Actions.rightMouseClickDown) then
self.mXorig = System.GetHardwareMouseX();
self.vFirstCamAng = self.cam:GetAngles();
self.bAimEnabled = true;
elseif (action == self.Actions.rightMouseClickUp) then
self.bAimEnabled = false;
elseif ((action == self.Actions.mouseX or action == self.Actions.mouseY) and self.bAimEnabled) then
local mX = System.GetHardwareMouseX();
local mDiff = self.mXorig - mX;
local newCamAngle = g_Vectors.temp_v1;
CopyVector(newCamAngle, self.vFirstCamAng);
local rotationAmt = ((mDiff*g_Pi)/2048);
newCamAngle.z = newCamAngle.z + rotationAmt;
newCamAngle.z = clamp(newCamAngle.z, self.rotRightLimit, self.rotLeftLimit);
self.cam:SetAngles(newCamAngle); --use this to set rotation vector for addimpulse
elseif (action == self.Actions.arrowRight) then
if (self.vCamPos.x < 215.3) then --xLimitRight
FastSumVectors(self.vCamPos, self.vCamPos, self.Properties.camJumpRight);
self.cam:SetLocalPos(self.vCamPos);
end
elseif (action == self.Actions.arrowLeft) then
if (self.vCamPos.x > 214.4) then --xLimitLeft
SubVectors(self.vCamPos, self.vCamPos, self.Properties.camJumpRight);
self.cam:SetLocalPos(self.vCamPos);
end
end
end
local command = HUD.GetLastFSCommand();
if (command ~= "") then
local arg = HUD.GetLastFSArgs();
if (command == self.FSCommands.throw) then
self.time = tonumber(string.match(arg, "%d+ "));
self.vecX = tonumber(string.match(arg, "[%s][%-]?[%d]+[%s]"));
self.vecY = math.abs(tonumber(string.match(arg, "[%s][%-]?[%d]+$")));
if (self.vecY == 0) then self.vecY = .5 end;
self.spinStrength = (self.vecX/self.vecY);
if (self.time ~= 0) then
self.impulsePow = 50 + ( clamp((1000 - self.time), 0, 600 ) / 12); --range pow from 50 to 100
else
self:ShowController();
end
local strengthCheck = math.abs(self.spinStrength); --stronger spin = shorter break point distance
if (strengthCheck < .5) then
self.breakPtDist = 9;
elseif (strengthCheck < 1) then
self.breakPtDist = 7;
elseif (strengthCheck < 2) then
self.breakPtDist = 5;
else
self.breakPtDist = 3;
end
self:GotoState("ThrowBall");
elseif (command == self.FSCommands.timedOut) then
ARDebugMessage("Oops! Too slow.", 5);
self:ShowController(); --restart controller
end
end
HUD.ClearLastFSCommand();
HUD.ClearLastAction();
end,
OnEndState = function(self)
self:HideController();
end,
}
ThrowBall
- throw the bowling ball with an impulse using the camera direction and parameters from the throw controller
BowlingGame.ThrowBall =
{
OnBeginState = function(self)
local camX = (self.cam:GetWorldPos()).x;
local ballPos = g_Vectors.temp_v2;
CopyVector(ballPos, self.vInitBallPos);
ballPos.x = camX;
self.ball:SetWorldPos(ballPos);
local impulseDir = g_Vectors.temp_v3;
local rotation_vec = self.cam:GetDirectionVector();
NormalizeVector(rotation_vec);
CopyVector(impulseDir, rotation_vec);
self.spinImp = clamp(self.spinStrength, -5, 5);
self.bRollingBackwards = false;
self.lastDist = 0;
self:Throw(impulseDir);
end,
OnUpdate = function(self,time)
local dist = (self.ball:GetWorldPos()).y - self.vInitBallPos.y;
--add impulse each frame to create curve (fake the breaking point)
if ((not self.bRollingBackwards) and (dist > self.breakPtDist and dist < 18)) then
self.ball:AddImpulse(-1, nil, g_Vectors.v000, 0, 0, g_Vectors.v001, self.spinImp * .2);
self.ball:AddImpulse(-1, nil, g_Vectors.v000, 0, 0, g_Vectors.v010, self.spinImp * .8);
end
if (dist < self.lastDist) then
self.bRollingBackwards = true;
end
if (self.bRollingBackwards and dist < 18) then
self.ball:AddImpulse(-1, nil, g_Vectors.v100, 1, 0, g_Vectors.v100, self.Properties.xAngImpMul);
end
if (self.ball:GetSpeed() <= 0.3) then
if (self.vInitBallPos.y ~= (self.ball:GetWorldPos()).y) then
self:GotoState("ResetAfterThrow");
end
end
self.lastDist = dist;
end,
OnEndState = function(self)
end,
}
ResetAfterThrow
This state:
- updates/saves the score for this frame
- reset the pins
- reset the ball
- transition to the PrepareToThrow state, or ClearLane if the game is over
BowlingGame.ResetAfterThrow =
{
OnBeginState = function(self)
local down=self:CheckPins();
self:RecordScore(down);
self.bScoreWasRecorded = true;
self:ResetPins();
self:ResetBall();
if (self.bGameIsOver) then
self:GotoState("ClearLane");
else
self:GotoState("PrepareToThrow");
end
end,
OnEndState = function(self)
end,
}
ClearLane
This end-of-game state:
- hides the bowling controls
- displays the play-again button
The OnUpdate callback checks if the play-again button has been clicked. If so, transition back to the Init state.
BowlingGame.ClearLane =
{
OnBeginState = function(self)
self:GetEntities();
self:HideController();
self:ShowPlayAgain();
end,
OnUpdate = function(self,time)
if (HUD.GetLastFSCommand() == self.FSCommands.playagain) then
HUD.UnloadFlash(self.hud_scoreboard);
self:GotoState("Init");
elseif (HUD.GetLastFSCommand() == self.FSCommands.exit) then
self:GotoState("Finish");
end
HUD.ClearLastFSCommand();
end,
OnEndState = function(self)
self:HidePlayAgain();
end,
}
Finish
The finish state performs some good-citizen cleanup on itself:
- deactivates the custom camera
- removes (and completely unloads from memory) the HUD
- turns off calls to OnUpdate callbacks
BowlingGame.Finish =
{
OnBeginState = function(self)
self.cam:Stop();
HUD.UnloadFlash(self.hud_scoreboard);
self:Activate(0);
end,
}
Functions
Here are the support functions used by the states.
- Show the throw controller
- Make it mouse sensitive
- Call the Actionscript function start in the throw controller
function BowlingGame:ShowController() HUD.ShowFlash(self.hud_controller); HUD.ModalFlash(self.hud_controller); HUD.InvokeFlashMethod(self.hud_controller,"start"); end
- Hide the throw controller
- Turn off mouse sensitivity (and hide the cursor)
- Call the Actionscript function Initialize in the throw controller
function BowlingGame:HideController() HUD.HideFlash(self.hud_controller); HUD.NonModalFlash(); HUD.InvokeFlashMethod(self.hud_controller,"control_mc.initialize"); end
- Show the play-again button
- Make it mouse sensitive (and show the cursor)
function BowlingGame:ShowPlayAgain() HUD.ShowFlash(self.playagain); HUD.ModalFlash(self.playagain); end
- Hide the play-again button
- Turn off mouse-sensitivity (and hide the cursor)
function BowlingGame:HidePlayAgain() HUD.HideFlash(self.playagain); HUD.NonModalFlash(); end
- add an impulse to the bowling ball
function BowlingGame:Throw(impulseDir) self.ball:AddImpulse(-1, nil, impulseDir, self.impulsePow, 0, g_Vectors.v011, self.spinImp); end
- reset the ball to its saved initial position
function BowlingGame:ResetBall()
if (self.bScoreWasRecorded) then
for i,v in pairs(self.balls) do --reset BALL for debug
local entity=v.entity;
entity:SetWorldPos(v.pos);
entity:SetWorldAngles(v.ang);
entity:SetScale(v.scale);
entity:AwakePhysics(1);
end
end
end
- reset the pins to their saved initial positions
function BowlingGame:ResetPins()
if (self.bScoreWasRecorded) then
if (self.bPinsShouldReset) then --true unless set false according to state in RecordScore()
for i,v in pairs(self.pins) do
local entity=v.entity;
entity:SetWorldPos(v.pos);
entity:SetWorldAngles(v.ang);
entity:SetScale(v.scale);
entity:AwakePhysics(1);
end
end
end
end
- return the number of downed pins
function BowlingGame:CheckPins(frameTime)
local down=0;
for i,v in pairs(self.pins) do
local entity=v.entity;
local pinup=vecNormalize(entity:GetDirectionVector(2));
if (vecDot(pinup, g_Vectors.up)<0.97 or ((entity:GetWorldPos()).y ~= v.pos.y)) then
down=down+1;
end
end
return down;
end
- this is the complicated one - it implements the bowling scoring rules
function BowlingGame:RecordScore(down)
self.bPinsShouldReset = true;
local lastFrameTot = 0; --for scoring after strikes/spares
local lastFrameNum = 0;
local frameTot = 0; --for directly scoring frame
if (self.scoreState==self.ROLLING_FIRST_BALL) then
if (down==10) then --strike
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball1","X");
self.rollingFrame = self.rollingFrame + 1;
self.scoreState = self.STRIKE_LAST_BALL;
else
self.firstBallInFrame = down;
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball1",""..self.firstBallInFrame.."");
self.scoreState = self.ROLLING_SECOND_BALL;
self.bPinsShouldReset = false;
end
elseif (self.scoreState==self.ROLLING_SECOND_BALL) then
down = down - self.firstBallInFrame;
if (down < 0) then down = 0 end;
if (self.rollingFrame == self.finalFrameNumber) then
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball2",""..down.."");
frameTot = self:AddFrame (self.firstBallInFrame + down);
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."score",""..frameTot.."");
HUD.SetFlashVariable(self.hud_scoreboard,"totalScore",""..frameTot.."");
self.rollingFrame = self.rollingFrame + 1;--to end game
elseif (self.firstBallInFrame + down == 10) then --spare
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball2","/");
self.rollingFrame = self.rollingFrame + 1;
self.scoreState = self.SPARE_LAST_BALL;
else --miss
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball2",""..down.."");
frameTot = self:AddFrame(self.firstBallInFrame + down);
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."score",""..frameTot.."");
self.rollingFrame = self.rollingFrame + 1;
self.scoreState = self.ROLLING_FIRST_BALL;
end
elseif (self.scoreState==self.SPARE_LAST_BALL) then
lastFrameTot = self:AddFrame (10 + down);
lastFrameNum = self.rollingFrame - 1;
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..lastFrameNum.."score",""..lastFrameTot.."");
if (down == 10) then
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball1","X");
self.rollingFrame = self.rollingFrame + 1;
self.scoreState = self.STRIKE_LAST_BALL;
else
self.firstBallInFrame = down;
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball1",""..self.firstBallInFrame.."");
self.scoreState = self.ROLLING_SECOND_BALL;
self.bPinsShouldReset = false;
end
elseif (self.scoreState==self.STRIKE_LAST_BALL) then
if (down == 10) then --strike after a strike
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball1","X");
self.rollingFrame = self.rollingFrame + 1;
self.scoreState = self.TWO_CONSEC_STRIKES;
else
self.firstBallInFrame = down;
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball1",""..self.firstBallInFrame.."");
self.scoreState = self.STRIKE_2_BALLS_AGO;
self.bPinsShouldReset = false;
end
elseif (self.scoreState==self.TWO_CONSEC_STRIKES) then
lastFrameTot = self:AddFrame(20 + down);
lastFrameNum = self.rollingFrame - 2;
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..lastFrameNum.."score",""..lastFrameTot.."");
if (down == 10) then
if (self.rollingFrame == self.finalFrameNumber) then
lastFrameTot = self:AddFrame(10 + down);
lastFrameNum = self.rollingFrame - 1;
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..lastFrameNum.."score",""..lastFrameTot.."");
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball1","X");
frameTot = self:AddFrame (down);
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."score",""..frameTot.."");
HUD.SetFlashVariable(self.hud_scoreboard,"totalScore",""..frameTot.."");
ARDebugMessage("Game Over"),5);
self.bGameIsOver = true;
else
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball1","X");
self.rollingFrame = self.rollingFrame + 1;
end
else
self.firstBallInFrame = down;
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball1",""..self.firstBallInFrame.."");
self.scoreState = self.STRIKE_2_BALLS_AGO;
self.bPinsShouldReset = false;
end
elseif (self.scoreState==self.STRIKE_2_BALLS_AGO) then
down = down - self.firstBallInFrame;
if (down < 0) then down = 0 end;
lastFrameTot = self:AddFrame (10 + self.firstBallInFrame + down);
lastFrameNum = self.rollingFrame - 1;
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..lastFrameNum.."score",""..lastFrameTot.."");
if (self.firstBallInFrame + down == 10) then --spare
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball2","/");
if (self.rollingFrame == self.finalFrameNumber) then
lastFrameTot = self:AddFrame(10 + self.firstBallInFrame + down);
lastFrameNum = self.rollingFrame - 1;
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..lastFrameNum.."score",""..lastFrameTot.."");
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball1","X");
frameTot = self:AddFrame (self.firstBallInFrame + down);
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."score",""..frameTot.."");
HUD.SetFlashVariable(self.hud_scoreboard,"totalScore",""..frameTot.."");
ARDebugMessage("Game Over",5);
self.bGameIsOver = true;
else
self.rollingFrame = self.rollingFrame + 1;
self.scoreState = self.SPARE_LAST_BALL;
end
else
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."ball2",""..down.."");
frameTot = self:AddFrame (self.firstBallInFrame + down);
HUD.SetFlashVariable(self.hud_scoreboard,"frame"..self.rollingFrame.."score",""..frameTot.."");
if (self.rollingFrame == self.finalFrameNumber) then
HUD.SetFlashVariable(self.hud_scoreboard,"totalScore",""..frameTot.."");
ARDebugMessage("Game Over",5);
self.bGameIsOver = true;
else
self.rollingFrame = self.rollingFrame + 1;
self.scoreState = self.ROLLING_FIRST_BALL;
end
end
end
if (self.rollingFrame > self.finalFrameNumber) then
ARDebugMessage("Game Over",5);
HUD.SetFlashVariable(self.hud_scoreboard,"totalScore",""..self.totalScore.."");
self.bGameIsOver = true;
end
end
- add a score to the current frame total
function BowlingGame:AddFrame(toAdd)
self.totalScore = self.totalScore + toAdd;
if (#(self.frameScores) < self.finalFrameNumber) then
self.frameScores[#(self.frameScores) + 1] = self.totalScore; --(total score up to 1st unscored frame)
end
return self.totalScore;
end




