Golf
From Blue Mars Developer Guidebook
|
|
Contents |
Overview
The Golf minigame demonstrates the following:
- Action map loading and enabling, using ActionMap_Manager functions
- Cameras -- Golf has its own camera which combines other MiniCamera features (Game/Levels/AR/Common/Entities/ARGolfCamera.ent)
- Character animation control using Entity functions
- Character attachments using Entity
- Entity-in-area checking with Entity functions
- HUD functions for interface display and interaction
- Physics and particle effects using Entity functions
- Playing sound effects with Sound functions
- Playing track view sequences with Movie functions
- Registering entities as discussed in Minigames#Registration (Game/Levels/AR/Common/Entities/ARGolfGame_Supervisor.ent and ARGolfGame_SP.ent)
- Spawning entities with System functions
- A minigame in a level which does not rely on the standard avatar synchronization or navigation (in the golf level there is no ARDefaultCamera entity - sync'ing and navigation is handled by the minigame script)
First, we sketch out the overall flow of the game as a state-transition diagram. It has:
- Start and finish states
- An inner loop for repeatedly hitting the golf ball
- A loop for moving to the next hole in the course
- A play again option
and we have additional states for managing the camera movement, physics simulation and player animation.
Class Declaration
Here's the header of our script, Game/Levels/AR/Common/Scripts/Entities/Minigames/Golf/ARGolfGame_SP.lua, the entity class description including the list of state names from our state transition diagram, and our entity variables.
ARGolfGame_SP =
{
Properties =
{
bAutoSkipWalk = 1,
addresults = "ARGolfLeaderPostResults.html",
addgolfgame = "",
},
States =
{
"Start",
"ShowCourse",
"ShowHole",
"PrepareToShoot",
"Walking",
"PerformShot",
"Swing",
"Shoot",
"WaitForBallToSettle",
"ShotComplete",
"PerformGimme",
"GimmeSwing",
"GimmeShoot",
"GimmeComplete",
"HoleComplete",
"CourseComplete",
"Finish_ReturnToGolfMatching",
"Finish_ExitGolf",
},
Actions =
{
arrowDown = "golf_arrowdown",
arrowUp = "golf_arrowup",
mouseX = "golf_mousex",
rightMouseClickDown = "golf_mouserightbtndown",
rightMouseClickUp = "golf_mouserightbtnup",
},
Anims =
{
idleTee = "ARGolf_DriveIdle-01.dba",
idlePitch = "ARGolf_PitchIdle-01.dba",
idlePutt = "ARGolf_PuttIdle-01.dba",
idleStand = "ARGolf_StandIdle-01.dba",
swing = "ARGolf_DriveSwing-01.dba",
swing_soft = "ARGolf_DriveSwing-Soft.dba",
swing_hard = "ARGolf_DriveSwing-Hard.dba",
swingPitch_soft = "ARGolf_ChipSwing-01.dba",
swingPitch_hard = "ARGolf_PitchSwing-01.dba",
swingPutt_soft = "ARGolf_PuttSwing-Soft.dba",
swingPutt_hard = "ARGolf_PuttSwing-Hard.dba",
walkBrisk = "ARGolf_GolfWalk-01.dba",
reacNeutral = "ARGolf_GolfReactionGood-01.dba",
},
FSCommands = --Actionscript fscommand names
{
clubselect = "Golf:ClubSelect",
exit = "Golf:Exit",
shoot = "Golf:Shoot",
skip = "Golf:Skip",
powerline = "Golf:PowerLine",
popup_left = "Confirm:BtnLeft",
popup_right = "Confirm:BtnRight",
popup_x = "Confirm:CloseBtn",
score_left = "GolfScore:Play",
score_right = "GolfScore:Exit",
score_x = "GolfScore:Close",
startTutorial = "Golf:Tutorial",
closeTutorial = "Golf:CloseTutorial",
tutorialComplete = "Golf:TutorialComplete",
},
Hud =
{
main = "levels/ar/common/libs/ui/golf/main.swf",
skip = "levels/ar/common/libs/ui/golf/golf_skip_fullscreen.swf",
exit = "levels/ar/common/libs/ui/golf/golf_exit_fullscreen.swf",
infoDisplay = "levels/ar/common/libs/ui/golf/infoDisplay.swf",
score1 = "levels/ar/common/libs/ui/golf/golf_scoreboard1hole.swf",
score3 = "levels/ar/common/libs/ui/golf/golf_scoreboard3holes.swf",
tutorial = "levels/ar/common/libs/ui/golf/golf_tutorial.swf",
tutorialBtn = "levels/ar/common/libs/ui/golf/golf_tutorialBtn.swf",
confirmpopup = "libs/ui/ar/common/confirm_popup.swf",
},
Ball_props =
{
Physics_fairway = {
bRigidBodyActive = 1,
bPhysicalize = 1,
density = -1,
mass = 1.3,
},
Simulation_fairway = {
damping = 0.55,
max_time_step = 0.02,
sleep_speed = .65,
},
Physics_green = {
bRigidBodyActive = 1,
bPhysicalize = 1,
density = -1,
mass = 2.9,
},
Simulation_green = {
damping = .44,
max_time_step = 0.008,
sleep_speed = .53,
},
Model =
{
green = "levels/ar/common/objects/golf/golf_ball_proxSphere.cgf";
fairway = "levels/ar/common/objects/golf/golf_ball_proxGeo.cgf";
},
Mtl = "levels/ar/common/objects/golf/golf_ball",
},
Clubs =
{
{name="Driver",attachmentName="club_driver",dist=214,z=.258,fudge=2.63,clubReach=.7,filePath="levels/ar/common/objects/golf/golf_clubDriver.cgf"},
{name="3Wood",attachmentName="club_3wood",dist=196,z=.268,fudge=2.05,clubReach=.7,filePath="levels/ar/common/objects/golf/golf_club3w.cgf"},
{name="5Wood",attachmentName="club_5wood",dist=179,z=.404,fudge=1.70,clubReach=.7,filePath="levels/ar/common/objects/golf/golf_club5w.cgf"},
{name="4",attachmentName="club_4",dist=161,z=.364,fudge=1.60,clubReach=.7,filePath="levels/ar/common/objects/golf/golf_club4.cgf"},
{name="5",attachmentName="club_5",dist=152,z=.466,fudge=1.46,clubReach=.7,filePath="levels/ar/common/objects/golf/golf_club5.cgf"},
{name="6",attachmentName="club_6",dist=143,z=.577,fudge=1.38,clubReach=.7,filePath="levels/ar/common/objects/golf/golf_club6.cgf"},
{name="7",attachmentName="club_7",dist=134,z=.700,fudge=1.31,clubReach=.7,filePath="levels/ar/common/objects/golf/golf_club7.cgf"},
{name="8",attachmentName="club_8",dist=125,z=.839,fudge=1.26,clubReach=.7,filePath="levels/ar/common/objects/golf/golf_club8.cgf"},
{name="9",attachmentName="club_9",dist=111,z=1.000,fudge=1.20,clubReach=.7,filePath="levels/ar/common/objects/golf/golf_club9.cgf"},
{name="PW",attachmentName="club_PW",dist=96,z=1.192,fudge=1.15,clubReach=.7,filePath="levels/ar/common/objects/golf/golf_clubPitch.cgf"},
{name="SW",attachmentName="club_SW",dist=77,z=1.483,fudge=1.06,clubReach=.7,filePath="levels/ar/common/objects/golf/golf_clubSand.cgf"},
{name="Putter",attachmentName="club_putter",dist=40,z=.03,fudge=.75,clubReach=.6,filePath="levels/ar/common/objects/golf/golf_clubPutter.cgf"},
},
Sounds =
{
applause = "levels/ar/common/sounds/golf:golf_sfx:applause",
ballInHole = "levels/ar/common/sounds/golf:golf_sfx:ball_in_hole",
button = "levels/ar/common/sounds/golf:golf_sfx:button",
swingGrass = "levels/ar/common/sounds/golf:golf_sfx:hit_grass",
swingOnly = "levels/ar/common/sounds/golf:golf_sfx:swing_club",
swingPutt = "levels/ar/common/sounds/golf:golf_sfx:hit_putter_temp",
swingTee = "levels/ar/common/sounds/golf:golf_sfx:hit_driver",
theme = "levels/ar/common/sounds/golf:songs:theme",
theme_intro = "levels/ar/common/sounds/golf:songs:theme_intro",
},
Editor=
{
Icon="AR_Default.bmp", --(in Editor/ObjectIcons)
ShowBounds=0,
},
ball = {},
cam = {},
Course = {Hole = {}},
holeFlag = {},
hole_tag = {},
holeScores = {},
info = {Name="",AvatarID=0,ClientID=0},
player = {},
svEnt = {},
tee_tag = {},
tee_sign = {},
actionCallbackId = 0,
cgfSlot = 0,
clubNum = 0,
distFromHole = 183,
fudgeFactor = 1,
gimmePuttDist = 1,
holeNum = 1,
holeSize = .08,
impulseMul = 0.321,
mXorig = 0,
particleSlot = 1,
pFX_fly = "golf_fx.ball_trail_white",
pFX_sand = "golf_fx.ball_hit_sand",
pFX_grass = "golf_fx.ball_hit_grass",
powerLineVal = 0,
shootImpulsePow = 0,
soundId_pan = 0,
soundId_end = 0,
strokeCount = 0,
timingSwing = .94,
timingPutt = 1.92,
timingSwing_drive = .94,
timingSwing_pitch = .25,
totalHoles = 0,
vClubShootDir = {x=0,y=0,z=0},
vShootImpulseDir = {x=0,y=0,z=0},
vInitBallPos = {x=0,y=0,z=0},
vInitBallAng = {x=90*g_Deg2Rad,y=0,z=0},
vBallPos = {x=0,y=0,z=0},
vLastBallPos = {x=0,y=0,z=0},
vPosLand = {x=0,y=0,z=0},
vBallToPlayer = {x=0,y=0,z=0},
vBallToHole = {x=0,y=0,z=0},
vHolePos = {x=0,y=0,z=0},
vPlayerPos = {x=0,y=0,z=0},
vTgtPos = {x=0,y=0,z=0},
vOrigPlayerAngle = {x=0,y=0,z=0},
vNewPlayerAngle = {x=0,y=0,z=0},
bAMapWasEnabled_Debug = false,
bAMapWasEnabled_Default = false,
bAMapWasEnabled_Player = false,
bAddedDownImp = false,
bAimEnabled = false,
bExitPopupShown = false,
bLowerCamEnabled = false,
bRaisedCamEnabled = false,
bBallInSand = false,
bBallOnGreen = false,
bBallOnTee = true,
bDoSkipToBall = false,
bEndMove = false,
bSetBallMvmntCheck = false,
bNoBallMvmnt = false,
bSetGimmeTimer = false,
bSkippedToBall = false,
bCgfSwitching = true,
bUsingBallGreen = false,
BLENDED_TIMERID = 100,
BALLSETTLE_TIMERID = 200,
SHOWSCOREBOARD_TIMERID = 300,
ENDCHOICE_TIMERID = 400,
SKIPWALK_TIMERID = 500,
ANIMPLAY_TIMERID = 600,
GIMMESTOP_TIMERID = 700,
GIMMEINHOLE_TIMERID = 800,
BALLMVMNT_TIMERID = 900,
BEAMSAFEGUARD_TIMERID = 1000,
SHOTLIMIT_TIMERID = 1100,
tempUpdatesDirection = 0,
}
Game Start
Single Player Game Start
Following the convention we established in Minigame Basics, we have a GameStart function that launches the game. This could be launched from a trigger in the level; however, in our case single player golf is launched and managed by the ARGolfSupervisor, described below. When the user chooses single player mode, the golf supervisor spawns the single player entity and calls its GameStart, on receipt of the SinglePlayerCallback.
function ARGolfGame_SP:GameStart()
self:Activate(1);
ARDebug(3);
if (not ActionMapManager.IsActionMapLoaded("ARGolf")) then
ActionMapManager.LoadFromXML("levels/ar/common/libs/config/golfProfile.xml");
end
System.SetCVar("r_DisplayInfo", 0); --in Editor/devmode
self:GotoState("Start");
end
- Call the Entity Activate function to ensure that the script's OnUpdate callbacks are invoked every frame
- Call the ARDebug function to turn on the lua debugger and set the log verbosity level (during development)
- Load the Golf Action Map with the ActionMap_Manager function LoadFromXML, if it is not already loaded
- Turn off the debug display info (top right corner of screen) with a Console Variable (during development)
- Launch the game by transitioning to its start state
Golf Supervisor
Here's how the golf supervisor is set up, with regard to the single player mode:
ARGolfSupervisor =
{
Properties =
{
sMasterEntityClass = "ARGolfGame_Master",
sPlayerEntityClass = "ARGolfGame_MP",
sSinglePlayerEntityClass = "ARGolfGame_SP",
sGameClass = "AR:Golf",
sGameMenuMovie = "",
bAutoStart = 1,
--in addition to supervisor base vars:
fileCourseInfoLua = "levels/ar/common/scripts/entities/minigames/golf/CourseInfo_orig.lua",
fTimeOfDay = 11.25,
sCourseInfoLuaTable = "CourseInfo_orig",
sExitToLevel = "AR_Startup",
},
Editor =
{
Icon = "AR_Default.bmp",
},
Course = {Seq = {}, Hole = {},}, --table for course info
SinglePlayerEntity = {},
}
ARMiniGameSupervisorBase.Setup(ARGolfSupervisor);
function ARGolfSupervisor:OnSpawn()
ARVirtualWorld.SetTimeOfDay(self.Properties.fTimeOfDay);
end
--additions to supervisor base:
function ARGolfSupervisor:SinglePlayerCallback()
self:GetCourseInfo(); --gameHoles. in joincb, will get from roomData. here, defaults to section 1
if self.Properties.sSinglePlayerEntityClass then
local ent = System.SpawnEntity {class = self.Properties.sSinglePlayerEntityClass,
name = self:GetName() .. "_player",
position = self:GetPos(),
}
ent.SvShowMatching = function () --show matching screen again
self.SinglePlayerEntity:DeleteThis();
self:GotoState("Available");
self:GameStart();
end
ent.SvExitGolf = function () --exit, return to specific level
self.SinglePlayerEntity:DeleteThis();
self:GotoState("Available");
CryAction.ScheduleEndLevel(self.Properties.sExitToLevel);
end
self:PrepareSinglePlayerEntity(ent);
ent:SetCourseInfo(self.Course, self); --also return supervisor ent for IsEntityInsideArea check
ent:GotoState(ent.States[1]);
end
end
function ARGolfSupervisor:GetCourseInfo(section)
local game_ent = self;
Script.ReloadScript(game_ent.Properties.fileCourseInfoLua);
local course_info = self.Properties.sCourseInfoLuaTable;
local func_getCourseInfo = assert (loadstring("return ".. self.Properties.sCourseInfoLuaTable ..".Course"));
local entireCourse = {}; --all holes in course info file
if (func_getCourseInfo ~= nil) then
entireCourse = func_getCourseInfo();
end
self.Course = {Seq = {}, Hole = {},};
self.Course.sGameClass = self.Properties.sGameClass;
self.Course.Seq = entireCourse.Seq;
self.Course.section = section;
local gameHoles = {}
--receive "section" from multiplayerjoin cb, or assign default for single player:
if (self.Properties.sGameClass == "AR:Golf_Course1") then
if (section == "Seaside (Holes 1-3)") then --matches text displayed in drop-down menu of matching UI (MP only)
gameHoles = {1,2,3};
elseif (section == "The Sands (Holes 4-6)") then
gameHoles = {4,5,6};
elseif (section == "Shadyside (Holes 7-9)") then
gameHoles = {7,8,9};
elseif (section == "The Front Nine (Holes 1-9)") then
gameHoles = {1,2,3,4,5,6,7,8,9};
--SP in AR_Golf_Course_1 level:
else
gameHoles = {1,2,3};
self.Course.section = "Seaside (Holes 1-3)";
end
--SP in AR_Golf level:
elseif (self.Properties.sGameClass == "AR:Golf") then
gameHoles = {1};
self.Course.section = "The Malihini";
end
for i = 1, #gameHoles do
self.Course.Hole[i] = entireCourse.Hole[gameHoles[i]];
end
end
function ARGolfSupervisor:PrepareSinglePlayerEntity(ent)
self.SinglePlayerEntity = ent;
function ent.DressUp(playerent, avatar_entity)
ARAvatar.ApplyCustomization(avatar_entity, ARMMOAux.AvatarData.avatarItems.lod2);
local facedata = ARMMOAux.AvatarData.avatarData.faceData;
local cosmedata = ARMMOAux.AvatarData2 and ARMMOAux.AvatarData2.cosmeticsData;
ARAvatar.ApplyFace(avatar_entity, facedata, cosmedata);
end
ent:Activate(1);
end
...
ARMiniGameSupervisorBase.Setup initializes the supervisor entity (included in the Multiplayer_Minigame_API), and then we:
- Override the SinglePlayerCallback with golf-specific functionality
- Spawn the single player entity (ARGolfGame_SP class) with System SpawnEntity
- Define the SvShowMatching and SvExitGolf functions used by the Finish states below
- Define the DressUp function in PrepareSinglePlayerEntity() which grabs the avatar customizations
- Deliver the Course Info, specified in the supervisor's properties, to the single player entity:
- Use Script ReloadScript and loadstring to get the Course table in the course info file, which contains course-specific data.
- Call SetCourseInfo(), defined in the ARGolfGame_SP script, which copies the Course table and stores the supervisor entity (ARGolfGame_SP needs this for boundary checking with Entity IsEntityInsideArea)
function ARGolfGame_SP:SetCourseInfo(course, svEnt) self.Course = course; self.svEnt = svEnt; end
States
Start
The start state calls some game-specific initialization functions, described below, and then transitions to the "ShowCourse" state.
ARGolfGame_SP.Start =
{
OnBeginState = function(self)
if (not ActionMapManager.IsActionMapLoaded("ARGolf")) then
ActionMapManager.LoadFromXML("levels/ar/common/libs/config/golfProfile.xml");
end
self:GetEntities();
self:LoadFlash();
self.totalHoles = #(self.Course.Hole);
self:InitializeHole(self.holeNum);
self:PrecacheSounds();
if (not ActionMapManager.IsActionMapEnabled("ARGolf")) then
ActionMapManager.EnableActionMap("ARGolf", true);
end
self:DisableActionMaps();
Physics.SetMaxBounceImpulse(10);
local animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.idleStand;
self.player:StartAnimationAR(animFile, {blendTime=1, speed=1, loop=true});
self.bReceivedInventory = false;
if (not System.IsEditor()) then
self:GetClubInventory();
else
self:GotoState("ShowCourse");
end
end,
OnUpdate = function(self,time)
if (self.bReceivedInventory) then
self:GotoState("ShowCourse");
else
System.Log("checking club inventory");
end
end,
}
- 1. We load the ARGolf action map if it is not already loaded, using ActionMap_Manager IsActionMapLoaded and LoadFromXML.
- 2. GetEntities() initializes the variables that refer to other entities used in the game. The ball entity, placed in the level, is retrieved by name with a System function, and the ball trail particle effect is pre-loaded. Both the avatar and the camera are spawned with the System SpawnEntity function, and character attachments are created with Entity CreateBoneAttachment and SetAttachmentCGF functions in LoadClubs().
function ARGolfGame_SP:GetEntities()
self:ResetMemberVars();
self.ball = System.GetEntityByName("Ball1");
self.ball:SetViewDistUnlimited();
self.ball:PreLoadParticleEffect(self.pFX_fly); --this is really only necessary for a particle path outside of a Libs/Particles directory
self.ball:PreLoadParticleEffect(self.pFX_grass);
self.ball:PreLoadParticleEffect(self.pFX_sand);
local game_ent = self;
self:SpawnGolfer(game_ent);
if (not System.IsEditor()) then
self:DressUp(self.player); --add avatar face/clothing customizations, not available in the Editor
end
self:LoadClubs();
self:SpawnCam();
end
function ARGolfGame_SP:SpawnGolfer(game_ent)
local avatar_data = {};
local avatar_id = "";
if (not System.IsEditor()) then
avatar_data = MMO:GetLocalCharacterData();
self.player = ARAvatar:SpawnAvatar("avt".. avatar_data.ClientID, nil, nil, MMO:GetLocalCharacterData().Gender);
self.info = avatar_data;
self.info.Name = self.info.ShortName;
else
self.player = ARAvatar:SpawnAvatar("avt123", nil, nil, "F");
self.info.Name = self.player:GetName();
end
local animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.idleStand;
self.player:StartAnimationAR(animFile, {blendTime=.5, speed=1, loop=true});
--player callbacks:
self.player.OnBeginMove = function(self)
local animFile = "levels/ar/common/animations/human/" .. self:GetAvatarBaseModel() .. "/minigames/golf/" .. game_ent.Anims.walkBrisk;
self:StartAnimationAR(animFile, {blendTime=0, speed=1, loop=true});
end
self.player.OnEndMove = function(self)
game_ent:SetEndMove();
end
self.player.OnCancelMove = function(self)
game_ent:SetEndMove();
end
if (not System.IsEditor()) then --set animation timing
if (MMO:GetLocalCharacterData().Gender == "M") then
self.timingPutt = 1.92;
self.timingSwing = 1.00;
self.timingSwing_drive = 1.00;
self.timingSwing_pitch = 1.07;
end
end
end
function ARGolfGame_SP:LoadClubs()
for i = 1, #(self.Clubs) do
self.player:CreateBoneAttachment(0, "Bip01 Prop1", self.Clubs[i].attachmentName);
self.player:SetAttachmentCGF(0, self.Clubs[i].attachmentName, self.Clubs[i].filePath);
end
end
- The golf-specific camera, Levels/AR/Common/Scripts/Entities/Minigames/Golf/ARGolfCamera.lua, combines the functionality of other example cameras, including the MiniAttachCamera and the MiniFollowCamera. We spawn this ARGolfCamera, and start it at the specified attached offset position, in the direction the player is facing (uses the Entity AttachChild function).
function ARGolfGame_SP:SpawnCam()
local props = {
Follow = {
fDist = 2,
fHeightOffset = 1.9,
fRotDamping = 1.0,
targetOffset = {x=0,y=0,z=-.5},
nQueueSize = 4,
fMinHeight = .25,
},
Aim = {
fHeightOffset = .95,
fDist = 2.5,
fSideDist = 0.7,
},
}
local spawnParams_cam = {
class = "ARGolfCamera",
position = self.player:GetWorldPos();
orientation = self.player:GetDirectionVector();
properties = props,
}
self.cam = System.SpawnEntity(spawnParams_cam);
self.cam:Start();
self.cam:SetTarget(self.player);
end
- 3. LoadFlash() calls the HUD LoadFlash function to load various interface elements and buttons. Each of these flash movies is full screen size (1280x720), within which the interface elements are positioned with respect to the full screen. Alternatively, a flash movie can be positioned and scaled with DockFlash and SetFlashPos, as in the commented-out example below. By default, a loaded movie is docked with eFD_Fit, stretched to fit the screen.
function ARGolfGame_SP:LoadFlash() HUD.LoadFlash(self.Hud.main); HUD.LoadFlash(self.Hud.skip); HUD.LoadFlash(self.Hud.exit); HUD.LoadFlash(self.Hud.infoDisplay); HUD.LoadFlash(self.Hud.score1); HUD.LoadFlash(self.Hud.score3); HUD.LoadFlash(self.Hud.tutorialBtn); HUD.LoadFlash(self.Hud.tutorial); HUD.LoadFlash(self.Hud.confirmpopup); --example of setting position: --HUD.LoadFlash(self.Hud.skipBtn); --HUD.DockFlash(self.Hud.skipBtn,eFD_Pos); --HUD.SetFlashPos(self.Hud.skipBtn,.95,.9); end
- 4. InitializeHole() initializes the first hole using entities in the Course table, like hole-specific TagPoints which are used to calculate initial positions and distances. Also included in the Course table are area names and Id's used to delimit out-of-bounds, golf green and sand bunker zones, as well as the specified ARTeeSign map and "par" value. The InfoDisplay movie is updated with the hole number, par, and golfer name using the HUD SetFlashVariable function.
function ARGolfGame_SP:InitializeHole(n)
self.hole_tag = System.GetEntityByName(self.Course.Hole[n].hole_tag);
self.holeFlag = System.GetEntityByName(self.Course.Hole[n].holeflag);
self.tee_tag = System.GetEntityByName(self.Course.Hole[n].tee_tag);
if (self.Course.Hole[n].tee_sign) then
self.tee_sign = System.GetEntityByName(self.Course.Hole[n].tee_sign);
if (self.tee_sign ~= nil) then
self.tee_sign:SetSelectableFromGolf(true);
end
else
self.tee_sign = nil;
end
CopyVector(self.vInitBallPos, self.tee_tag:GetWorldPos());
self.vHolePos = self.hole_tag:GetWorldPos();
self.distFromHole = math.sqrt(DistanceSqVectors2d(self.vHolePos, self.vInitBallPos));
if (self.holeFlag:IsHidden()) then self.holeFlag:Hide(0) end;
self.ball:SetWorldPos(self.vInitBallPos);
self.ball:SetWorldAngles(self.vInitBallAng);
CopyVector(self.vLastBallPos, self.vInitBallPos);
CopyVector(self.vBallPos, self.vInitBallPos);
CopyVector(self.vTgtPos, self:FindTgtPos(self.vInitBallPos));
self.player:SetWorldPos(self.vTgtPos);
self.player:SetDirectionVector(self.vBallToHole);
self.bBallOnGreen = false;
self.bBallOnTee = true;
if (self.bCgfSwitching) then
self:SwapBallGreen(false);
end
HUD.SetFlashVariable(self.Hud.infoDisplay,"info_top","Hole ".. self.Course.Hole[n].actualHoleNum .." / Par ".. self.Course.Hole[n].par);
HUD.SetFlashVariable(self.Hud.infoDisplay,"info1_player","Golfer:");
HUD.SetFlashVariable(self.Hud.infoDisplay,"info1_name"," " .. self.info.Name);
end
- 5. PrecacheSounds() calls the Sound function to Precache specific sounds.
function ARGolfGame_SP:PrecacheSounds() Sound.Precache(self.Sounds.theme, 0); Sound.Precache(self.Sounds.theme_intro, 0); Sound.Precache(self.Sounds.swingTee, 0); Sound.Precache(self.Sounds.swingPutt, 0); Sound.Precache(self.Sounds.ballInHole, 0); end
- 6. The Golf Action Map is enabled with the ActionMap_Manager EnableActionMap function if it is not enabled. Then the other standard action maps (player, default and debug) are disabled in the same manner, storing the boolean return of IsActionMapEnabled to determine whether to enable this map at the end of the game.
function ARGolfGame_SP:DisableActionMaps()
if (ActionMapManager.IsActionMapEnabled("player")) then
ActionMapManager.EnableActionMap("player",false);
self.bAMapWasEnabled_Player = true;
end
if (ActionMapManager.IsActionMapEnabled("debug")) then
ActionMapManager.EnableActionMap("debug",false);
self.bAMapWasEnabled_Debug = true;
end
if (ActionMapManager.IsActionMapEnabled("default")) then
ActionMapManager.EnableActionMap("default",false);
self.bAMapWasEnabled_Default = true;
end
end
Golf Action Map
Below is the ARGolf action map (Levels/AR/Common/Libs/Config/golfProfile.xml). See ActionMap_Manager for a list of key names and activation modes.
<?xml version="1.0" encoding="utf-8"?>
<ActionMaps>
<actionmap name="ARGolf" version="21">
<action name="golf_mouseclick" onPress="1">
<key name="mouse1" />
</action>
<action name="golf_mouserightbtnup" onRelease="1">
<key name="mouse2" />
</action>
<action name="golf_mouserightbtndown" onPress="1">
<key name="mouse2" />
</action>
<action name="golf_mousex">
<key name="maxis_x" />
</action>
<action name="golf_mousey">
<key name="maxis_y" />
</action>
<action name="golf_arrowup" onPress="1">
<key name="up" />
<key name="np_8" />
</action>
<action name="golf_arrowdown" onPress="1">
<key name="down" />
<key name="np_2" />
</action>
</actionmap>
</ActionMaps>
Check Inventory
- 7. The last thing we do in the Start state is to check the player's inventory for any game-specific items. If a player has purchased a set of golf clubs in Beach City before starting a game, the script detects the items by using the Inventory.CheckInventory API. The callback function sets a flag which signals the OnUpdate to go to the next state, ShowCourse. Note: please find the updated game script in the developer sample data if you are interested in the code which handles in-game club selection and the subsequent affect on shot accuracy and power.
function ARGolfGame_SP:GetClubInventory()
Inventory.CheckInventory (
{self.clubItemID_proBlue, self.clubItemID_proPurple},
function (tbl)
for id, flag in pairs (tbl) do
System.Log((flag and "Player has " or "Player does not have ") .. id)
if (flag) then
if (id == self.clubItemID_proBlue) then
self.bSetIsAvailable_proBlue = true;
self.currentClubSet = 2;
end
if (id == self.clubItemID_proPurple) then
self.bSetIsAvailable_proPurple = true;
self.currentClubSet = 3;
end
end
end
self.bReceivedInventory = true;
end
);
end
ShowCourse
When the ShowCourse state is entered, we check whether this course has an opening course preview sequence, or shorter sequences for each hole. If the course has an opening sequence, we play it. If not, and if the course has per-hole sequences, we transition to the "ShowHole" state. Otherwise we transition to the "PrepareToShoot" state.
ARGolfGame_SP.ShowCourse =
{
OnBeginState = function(self)
self:HideInfoDisplay();
if (self.Course.Seq.bCourseHasOpeningSeq) then
Movie.PlaySequence(self.Course.Seq.opening_seq);
self.soundId_pan = Sound.PlayEx(self.Sounds.theme_intro, self.vHolePos, SOUND_2D, .8, 0, 0, SOUND_SEMANTIC_HUD);
Sound.SetFadeTime(self.soundId_pan, 1.0, 300);
self:ShowSkip();
self:ShowExit();
elseif (self.Course.Seq.bCourseHasPerHoleSeqs) then
self:GotoState("ShowHole");
else
self:ShowExit();
self:GotoState("PrepareToShoot");
end
end,
OnUpdate = function(self,time)
if (not Movie.IsPlaying(self.Course.Seq.opening_seq) and self.Course.Seq.bCourseHasOpeningSeq) then
if (self.Course.Seq.bCourseHasPerHoleSeqs) then
self:GotoState("ShowHole");
else
self:GotoState("PrepareToShoot");
end
end
end,
OnFSCommand = function(self,command,arg)
if (command == self.FSCommands.skip) then
if (self.Course.Seq.bCourseHasPerHoleSeqs) then
self:GotoState("ShowHole");
else
self:GotoState("PrepareToShoot");
end
elseif (command == self.FSCommands.exit) then
self:ShowPopupConfirmExit();
elseif (command == self.FSCommands.popup_left) then
self:HidePopup();
self:GotoState("Finish_ReturnToGolfMatching");
elseif (command == self.FSCommands.popup_right or command == self.FSCommands.popup_x) then
self:HidePopup();
end
end,
OnEndState = function(self)
self:HideSkip();
if (self.Course.Seq.bCourseHasOpeningSeq) then
Sound.StopSound(self.soundId_pan);
Sound.SetFadeTime(self.soundId_pan, 0.0, 8000);
Movie.StopAllSequences();
end
self:ShowInfoDisplay();
end,
}
- We check whether this course has an opening sequence with the bCourseHasOpeningSeq value stored in the Course table (obtained from the ARGolfSupervisor properties). If so, we:
- Play the opening sequence (a preview of the golf course, created with the Editor's TrackView) with Movie PlaySequence.
- Play sound with Sound PlayEx, and use SetFadeTime to prevent popping or clicking.
- Display the Skip button (described below).
- Transition to the next state if the OnUpdate function finds the sequence is finished, or the OnFSCommand callback is received from the Skip button, or the Exit button is clicked and confirmed to exit the game. The next state is ShowHole if the course has per-hole sequences, otherwise the next state is PrepareToShoot (unless the game is exited, which would bring us to the Finish_ReturnToGolfMatching state).
function ARGolfGame_SP:ShowSkip() HUD.ShowFlash(self.Hud.skip); HUD.HandleFlashEvents(self.Hud.skip); HUD.AddFSCommandListener(self.Hud.skip,self.id); end
- We display the Skip button with the HUD ShowFlash function.
- HandleFlashEvents causes the movie to receive mouse events.
- AddFSCommandListener causes the movie to send fscommands to this script, received by the OnFSCommand callback.
skipBtn.onRelease = function()
{
fscommand("Golf:Skip", "");
}
The Actionscript in the Flash movie just issues an fscommand (with an empty argument) upon releasing the Skip button.
function ARGolfGame_SP:HideSkip() HUD.HideFlash(self.Hud.skip); HUD.RemoveFSCommandListener(self.Hud.skip,self.id); end
When we exit the ShowCourse state, we hide the Skip button with HUD HideFlash and remove the listener with RemoveFSCommandListener.
ShowHole
Like the ShowCourse state, ShowHole checks whether the course has per-hole sequences and, if so, starts the sequence. Otherwise we transition to the next state, PrepareToShoot.
ARGolfGame_SP.ShowHole =
{
OnBeginState = function(self)
self:HideInfoDisplay();
self.bShowedHole = false;
if (self.Course.Seq.bCourseHasPerHoleSeqs) then
Movie.PlaySequence(self.Course.Hole[self.holeNum].overhead_seq);
else
self:GotoState("PrepareToShoot");
end
end,
OnUpdate = function(self,time)
if (not Movie.IsPlaying(self.Course.Hole[self.holeNum].overhead_seq) and (not self.bShowedHole)) then
self.bShowedHole = true;
self:GotoState("PrepareToShoot");
end
end,
OnFSCommand = function(self,command,arg)
if (command == self.FSCommands.exit) then
self:ShowPopupConfirmExit();
elseif (command == self.FSCommands.popup_left) then
self:HidePopup();
self:GotoState("Finish_ReturnToGolfMatching");
elseif (command == self.FSCommands.popup_right or command == self.FSCommands.popup_x) then
self:HidePopup();
end
end,
OnEndState = function(self)
self:ShowInfoDisplay();
self:ShowExit();
end,
}
PrepareToShoot
In the PrepareToShoot state, the player aims and chooses the swing power. The first time the state is entered, we display a "tutorial" button which brings up the golf tutorial. Each time the state is entered, as a safeguard, if the ball is already in the hole we go to the HoleComplete state. If the ball is very close to the hole we grant the player an automatic putt (a "gimme") and go to the PerformGimme state. Otherwise:
- The "main" HUD is displayed in ShowMain(), with HUD ShowFlash, HandleFlashEvents and AddFSCommandListener, as in ShowSkip() above.
- A default club is chosen in FindProperClub(), based on distance to the hole.
- An ActionScript function in the "main" Flash movie is called with HUD InvokeFlashMethod to display the default club. This is done in SetClub(), which is called by FindProperClub().
- The HUD RegisterActionCallback function is called to direct actions to this state's OnAction callback.
- The camera is set to the aim position with the ARGolfCamera's AttachTo() function.
ARGolfGame_SP.PrepareToShoot =
{
OnBeginState = function(self)
if (self.strokeCount == 0 and self.holeNum == 1) then
self:ShowTutorialBtn();
end
local ballFromHole = math.sqrt(DistanceSqVectors2d(self.vHolePos, self.ball:GetWorldPos()));
if (self.distFromHole < self.holeSize and self.ball:GetWorldPos().z < self.vHolePos.z) then
self:GotoState("HoleComplete");
elseif (self.distFromHole < self.gimmePuttDist) then --"gimme" automatic putt
self:FindProperClub();
self.holeFlag:Hide(1);
self.cam:Aim();
self:GotoState("PerformGimme");
else
self:FindProperClub();
self.bAimEnabled = false;
self.bLowerCamEnabled = false;
self.bRaisedCamEnabled = false;
self:ShowMain();
self.actionCallbackID = HUD.RegisterActionCallback (self.PrepareToShoot.OnAction, self);
if (self.distFromHole < 3) then
self.holeFlag:Hide(1);
elseif (self.distFromHole > 5) then
self.holeFlag:Hide(0);
end
self.cam:Aim();
end
self.tempUpdatesDirection = 0;
end,
OnAction = function(self, action, activationMode) --handle mouse action
if (action == self.Actions.rightMouseClickDown) then
self.vPlayerPos = self.player:GetWorldPos();
SubVectors(self.vBallToPlayer, self.vPlayerPos, self.vBallPos);
self.mXorig = System.GetHardwareMouseX();
self.vOrigPlayerAngle = self.player:GetAngles();
self.bAimEnabled = true;
elseif (action == self.Actions.rightMouseClickUp) then
self.bAimEnabled = false;
elseif ((action == self.Actions.mouseX) and self.bAimEnabled) then
local mX = System.GetHardwareMouseX();
local mDiff = self.mXorig - mX;
CopyVector(self.vNewPlayerAngle, self.vOrigPlayerAngle);
local rotationAmt = ((mDiff*g_Pi)/2048);
local vNewPlayerPos = g_Vectors.temp_v1;
RotateVectorAroundR(vNewPlayerPos, self.vBallToPlayer, g_Vectors.up, rotationAmt);
FastSumVectors(vNewPlayerPos, self.vBallPos, vNewPlayerPos);
vNewPlayerPos.z = System.GetTerrainElevation(vNewPlayerPos); --reset on terrain
self.player:SetWorldPos(vNewPlayerPos);
self.vNewPlayerAngle.z = self.vNewPlayerAngle.z + rotationAmt;
self.player:SetAngles(self.vNewPlayerAngle);
elseif (action == self.Actions.arrowDown) then --perspective cam:
if (self.bRaisedCamEnabled) then --return raised to neutral
self.cam:AdjustHeight(nil, true);
self.bRaisedCamEnabled = false;
HUD.ShowFlash(self.Hud.main);
if (self.bBallOnGreen) then self.player:Hide(0) end --unhide avatar
elseif (not self.bLowerCamEnabled) then --lower
self:RaiseOrLowerCam(.3);
self.bLowerCamEnabled = true;
HUD.HideFlash(self.Hud.main);
self.player:Hide(1);
end
elseif (action == self.Actions.arrowUp) then
if (self.bLowerCamEnabled) then -- return lowered to neutral
--self.cam:Aim();
self.cam:AdjustHeight(nil, true);
self.bLowerCamEnabled = false;
HUD.ShowFlash(self.Hud.main);
self.player:Hide(0);
elseif (not self.bRaisedCamEnabled) then --raise
self:RaiseOrLowerCam(8);
self.bRaisedCamEnabled = true;
HUD.HideFlash(self.Hud.main);
if (self.bBallOnGreen) then self.player:Hide(1) end --hide avatar
end
end
end,
...
The OnAction callback handles the following:
- Right-click-drag mouse actions rotate the player's aim direction
- The right-click-down action enables aiming and we get the current location of the mouse with System GetHardwareMouseX.
- The horizontal mouse movement is tracked, and the player's new position is calculated with RotateVectorAroundR and other functions in Game/Scripts/Utils/Math.lua, and set with Entity SetWorldPos after we get the z-height of the new location with System GetTerrainElevation. In this case, the interpolated height returned by GetTerrainElevation gets better results than GetTerrainZ, which returns the height of the nearest terrain grid point.
- Aiming is disabled when the right-click-up action is received.
- Up/Down arrow actions temporarily raise/lower the camera for perspective.
- While raised or lowered, we hide the "main" Flash movie.
- RaiseOrLowerCam() passes the desired offset height to the ARGolfCamera, which adjusts its height by changing both the Aim.fHeightOffset property and the field-of-view with Game.SetViewFov (below).
function ARGolfGame_SP:RaiseOrLowerCam(zOffset)
if (zOffset > 5) then --raise
if (self.bBallOnGreen and self.distFromHole < 4) then
zOffset = zOffset/6;
else
zOffset = zOffset/3;
end
end
self.cam:AdjustHeight(zOffset, false);
end
function ARGolfCamera:AdjustHeight(zOffset, bNeutral)
if (zOffset) then
self.Properties.Aim.fHeightOffset = zOffset;
else
self.Properties.Aim.fHeightOffset = self.origAimHeightOffset;
end
if (bNeutral) then
self:SetFov(60);
else
self:SetFov(45);
end
end
function ARGolfCamera:SetFov(deg)
local golfFov = g_Deg2Rad * deg;
Game.SetViewFov(self.viewId, golfFov);
end
...
OnFSCommand = function(self,command,arg)
if (command == self.FSCommands.powerline) then
self:SetShootPower(tonumber(arg));
elseif (command == self.FSCommands.clubselect) then
self.player:PlaySoundEvent(self.Sounds.button, g_Vectors.v000, g_Vectors.v010, SOUND_2D, SOUND_SEMANTIC_HUD);
local clubNum = 0; --translate club name to num
if (arg == "Driver") then clubNum = 1;
elseif (arg == "3Wood") then clubNum = 2;
elseif (arg == "5Wood") then clubNum = 3;
elseif (arg == "4") then clubNum = 4;
elseif (arg == "5") then clubNum = 5;
elseif (arg == "6") then clubNum = 6;
elseif (arg == "7") then clubNum = 7;
elseif (arg == "8") then clubNum = 8;
elseif (arg == "9") then clubNum = 9;
elseif (arg == "PW") then clubNum = 10;
elseif (arg == "SW") then clubNum = 11;
elseif (arg == "Putter") then clubNum = 12;
end
self:SetClub(clubNum, false); --2nd argument, false, does not invoke club set method in HUD
elseif (command == self.FSCommands.shoot) then --impulse calculations and power adjustments:
local angOff = 0;
local powOff = 0;
local vRotation = g_Vectors.temp_v2;
--INITIAL AIM DIR
CopyVector(vRotation, self.player:GetDirectionVector());
self.vClubShootDir.x = vRotation.x;
self.vClubShootDir.y = vRotation.y;
NormalizeVector(self.vClubShootDir);
--ROTATIONAL OFFSET accd'g to random factor (& skill)
if (self.bBallInSand) then
if (self.clubNum == 11) then angOff = randomF(-7,7); --using Sand Wedge
else angOff = randomF(-12, 12); --penalty for not using SW in sand
end
elseif (self.clubNum < 4) then --woods
angOff = randomF(-5,5);
elseif (self.clubNum < 12) then --irons, PW, SW if not in sand (not putter)
angOff = randomF(-3,3);
else
angOff = nil;
end
--RED ZONE power selection
if (self.powerLineVal > 72) then
powOff = 1.5 * (randomF((self.powerLineVal-72)/4, self.powerLineVal-72));
self:ReportPowerAccuracy(powOff*100/self.powerLineVal);
self.powerLineVal = self.powerLineVal - powOff;
--RED ZONE addn'l ROTATIONAL OFFSET
if (angOff == nil) then --case: putter
angOff = randomF(-3,3);
else
angOff = 2 * angOff;
end
end
--SAND additional power penalty
if (self.bBallInSand) then
self.powerLineVal = self.powerLineVal * .60;
end
if (self.clubNum == 12) then
self.powerLineVal = self.powerLineVal + 5;
self.shootImpulsePow = clamp(self.impulseMul*self.powerLineVal*self.fudgeFactor, 3, 100);
else
self.shootImpulsePow = clamp(self.impulseMul*self.powerLineVal*self.fudgeFactor, 5, 100);
end
if (angOff ~= 0 and angOff ~= nil) then
vRotation.z = vRotation.z + (g_Deg2Rad * angOff);
self:ReportShotAccuracy(angOff);
end
RotateVectorAroundR(self.vShootImpulseDir, self.vClubShootDir, g_Vectors.up, vRotation.z);
self:GotoState("PerformShot");
elseif (command == self.FSCommands.exit) then
self:ShowPopupConfirmExit();
elseif (command == self.FSCommands.popup_left) then
self:HidePopup();
self:GotoState("Finish_ReturnToGolfMatching");
elseif (command == self.FSCommands.popup_right or command == self.FSCommands.popup_x) then
self:HidePopup();
elseif (command == self.FSCommands.startTutorial) then
self:ShowTutorial();
if (self.tee_sign ~= nil) then
self.tee_sign:ResetSign();
end
elseif (command == self.FSCommands.closeTutorial or command == self.FSCommands.tutorialComplete) then
self:HideTutorial();
end
end,
...
The OnFSCommand callback handles fscommands sent from the "main" Flash movie when a new club is chosen, a power value is selected or the Shoot button is invoked. An argument can be sent along with the fscommand from the movie's ActionScript. E.g., the ActionScript that rotates the club selector left or right issues an fscommand with the new club type:
fscommand("Golf:ClubSelect", clubType);
We translate this club type to a club number (E.g., "if (arg == "Driver") then clubNum = 1..." above), then pass this number to the SetClub function:
function ARGolfGame_SP:SetClub(num, bInvokeFlash)
self.clubNum = num;
self:ShowClub(num);
self.vClubShootDir = {x=0,y=1,z=self.Clubs[num].z};
self.fudgeFactor = self.Clubs[num].fudge;
if (bInvokeFlash) then --cause hud to display club
local numAS = num - 1;
HUD.InvokeFlashMethod(self.Hud.main,"_root.setClubType",numAS);
end
self:RepositionPlayerWithClub(self.vBallPos, self.player:GetWorldPos(), num);
local animFile = "";
if (num == 12) then --putter
animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.idlePutt;
self.player:StartAnimationAR(animFile, {blendTime=.5, speed=1, loop=true});
elseif (self.clubNum <10) then --woods and irons (except pw/sw)
animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.idleTee;
self.player:StartAnimationAR(animFile, {blendTime=.5, speed=1, loop=true});
else --pw/sw
animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.idlePitch;
self.player:StartAnimationAR(animFile, {blendTime=.5, speed=1, loop=true});
end
end
function ARGolfGame_SP:ShowClub(num)
for i,v in pairs(self.Clubs) do
self.player:HideAttachment(0, self.Clubs[i].attachmentName, true, true);
end
self.player:HideAttachment(0, self.Clubs[num].attachmentName, false, false);
end
In SetClub(), we show the proper club after cycling through and hiding every club with HideAttachment (instead of using the Entity HideAllAttachments function, which could also hide clothing and hair attachments for a moment). We only need the bInvokeFlash argument to be true if we are setting a default club and want the HUD to update with the default club selection. In this case bInvokeFlash is false, since the user has selected the club and Flash has already updated the HUD. Then we reposition the player according to the new club's reach distance, and start the idle-with-club animation with Entity StartAnimationAR.
Upon receiving the shoot fscommand, we calculate the shot impulse and power adjustments, call ReportShotAccuracy() which uses HUD SetFlashVariable to display Hook/Slice on the info display, and transition to the PerformShot state.
One thing to point out in the ShowTutorial function is the use of System.SetPostProcessFxParam to control the brightness of the screen while the tutorial plays:
function ARBowlingGame_SP:ShowTutorial()
HUD.ShowFlash(self.Hud.tutorial);
HUD.InvokeFlashMethod(self.Hud.tutorial,"startTutorial")
HUD.Modal(self.Hud.tutorial);
HUD.AddFSCommandListener(self.Hud.tutorial,self.id);
System.SetPostProcessFxParam("Global_User_Brightness", -0.2);
end
function ARBowlingGame_SP:HideTutorial()
HUD.Modeless();
HUD.HideFlash(self.Hud.tutorial);
HUD.RemoveFSCommandListener(self.Hud.tutorial,self.id);
System.SetPostProcessFxParam("Global_User_Brightness", 1.0);
end
...
OnUpdate = function(self,time)
if (self.tempUpdatesDirection < 5) then --reset dir for ai signal interference
if (self.tempUpdatesDirection == 4) then
self.player:SetDirectionVector(self.vBallToHole);
end
self.tempUpdatesDirection = self.tempUpdatesDirection + 1;
end
end,
OnEndState = function(self)
if (self.actionCallbackID) then
HUD.UnregisterActionCallback(self.actionCallbackID);
self.actionCallbackID = nil;
end
self:HideMain();
if (self.strokeCount == 0) then
self:HideTutorialBtn();
end
end,
}
The OnUpdate callback resets the player direction after a few frames to guard against interference from the AI system's OnCancelMove player callback, when we come to this state after skipping during the Walking state.
When we exit this state, we call HUD UnregisterActionCallback and HideMain() which hides the interface and removes the FSCommand Listener. We also hide the tutorial button if it was displayed.
Perform Shot
Performing the shot is broken up into a few states to handle physics and animation timing.
ARGolfGame_SP.PerformShot =
{
OnBeginState = function(self)
local animFile = "";
if (self.clubNum == 12) then --putter
if (not self.bUsingBallGreen) then --swap ball model (to sphere-defined proxy):
if (self.bCgfSwitching) then
self:SwapBallGreen(true);
end
end
if (self.strokeCount == 0) then --safeguard for using putter on tee
self:NewBallPos();
end
if (self.shootImpulsePow < 7) then
animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.swingPutt_soft;
self.player:StartAnimationAR(animFile, {blendTime=.50, speed=1, loop=false});
else
animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.swingPutt_hard;
self.player:StartAnimationAR(animFile, {blendTime=.50, speed=1, loop=false});
end
else
if (self.bCgfSwitching) then
if (self.bUsingBallGreen) then
self:SwapBallGreen(false);
end
end
self:NewBallPos();
if (self.clubNum <10) then --woods and irons, except pw/sw
self.timingSwing = self.timingSwing_drive;
if (self.shootImpulsePow < 15) then
animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.swing_soft;
self.player:StartAnimationAR(animFile, {blendTime=.50, speed=1, loop=false});
else
animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.swing_hard;
self.player:StartAnimationAR(animFile, {blendTime=.50, speed=1, loop=false});
end
else --pitching wedge/sand wedge
self.timingSwing = self.timingSwing_pitch;
if (self.shootImpulsePow < 10) then
animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.swingPitch_soft;
self.player:StartAnimationAR(animFile, {blendTime=.50, speed=1, loop=false});
else
animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.swingPitch_hard;
self.player:StartAnimationAR(animFile, {blendTime=.50, speed=1, loop=false});
end
end
end
self:SetTimer(self.BLENDED_TIMERID, 750); --move on to Swing state using GetAnimationTime for shoot timing
end,
OnTimer = function(self, timerId)
if (timerId == self.BLENDED_TIMERID) then
self:GotoState("Swing");
end
end,
}
On entering the PerformShot state, we:
- Load the proper ball, depending on whether we are putting, with SwapBallGreen(), described below.
- Nudge the ball above the terrain with NewBallPos().
- Blend into the proper swing animation, depending on the club category and swing power, with Entity StartAnimationAR.
- Set a timer with Entity SetTimer to delay the transition to the Swing state until after we have passed the animation blendTime.
function ARGolfGame_SP:NewBallPos()
local vRaiseBall = g_Vectors.temp_v2; --nudge ball up off terrain
vRaiseBall = {x=0,y=0,z=.008};
FastSumVectors(self.vBallPos, self.vBallPos, vRaiseBall);
end
function ARGolfGame_SP:SwapBallGreen(bGreen)
self.ball:FreeSlot(self.cgfSlot)
if (bGreen) then
self.ball:LoadObject(self.cgfSlot, self.Ball_props.Model.green)
self.bUsingBallGreen = true;
else
self.ball:LoadObject(self.cgfSlot, self.Ball_props.Model.fairway)
self.bUsingBallGreen = false;
end
self.ball:Physicalize(self.cgfSlot, PE_RIGID, self.Ball_props.Physics);
self.ball:SetPhysicParams(PHYSICPARAM_SIMULATION, self.Ball_props.Physics.Simulation);
self.ball:SetMaterial("levels/ar/common/objects/golf/golf_ball");
end
We switch between a cgf with a sphere-defined proxy (results in a better roll on the green) and a cgf with a mesh proxy (results in a better bounce on terrain).
- Entity FreeSlot removes the current cgf in the slot.
- Entity LoadObject loads the new cgf.
- Call Entity Physicalize and SetPhysicParams to Physicalize and set simulation properties.
- Set the material with Entity SetMaterial.
Swing
This state uses Entity GetAnimationTime to check if the swing animation has reached the point where the club should contact the ball; if so, we transition to the Shoot state. Note that for the return value of GetAnimationTime to be accurate, we come to this state after the animation blending finishes in the PerformShot state; else the first several return values here would report the animation time of the previous animation blending out.
A convenient way to determine this animation time: in the CryEngine Character Editor, select the animation and go to the Animation Control tab, pause the animation at the desired point, and read the time (in seconds) at the end of the "Animation Asset" path (not the "Progress"). If the path is too long, double-click on it to see the second count at the end.
ARGolfGame_SP.Swing =
{
OnUpdate = function(self,time)
local animTime = 0;
if (self.player:IsAnimationRunning(0,0)) then
animTime = self.player:GetAnimationTime(0,0);
if (self.clubNum == 12) then
if (animTime > self.timingPutt) then
Sound.Play(self.Sounds.swingPutt, self.vPlayerPos, SOUND_2D, SOUND_SEMANTIC_AMBIENCE_ONESHOT);
self:GotoState("Shoot");
end
else
if (animTime > self.timingSwing) then
Sound.Play(self.Sounds.swingTee, self.vPlayerPos, SOUND_2D, SOUND_SEMANTIC_AMBIENCE_ONESHOT);
self:GotoState("Shoot");
end
end
end
end,
}
Shoot
On entry, this state calls Entity EnablePhysics and ShootBall() to launch the golf ball by imparting an impulse and, if the shot is strong enough, uses the Entity LoadParticleEffect function to start a particle effect trail on the golf ball. Also a grass particle effect is started if the shot is not a putt on the green, or a sand particle effect is started if the shot is taking place in a sand bunker. The ball trail effect is loaded into entity slot 1, and the additional grass/sand effects use the next available entity slot (-1). We also use timers as safeguards for no ball movement and to limit very long shots.
The OnUpdate callback checks the golf ball speed and position, transitioning to the WaitForBallToSettle state upon finding the ball at zero speed. It also adds a downward impulse when the ball is over the hole, and ensures that the player idle animation plays when the player swing animation from the previous state has stopped.
ARGolfGame_SP.Shoot =
{
OnBeginState = function(self)
self.ball:EnablePhysics(true);
local bShowTrail = self:ShootBall();
if (bShowTrail and self.clubNum ~= 12) then -- ~12 = not using putter
self.particleSlot = self.ball:LoadParticleEffect(1, self.pFX_fly, {}); --white ball trail
if (not self.bBallOnTee and not self.bBallOnGreen and not self.bBallInSand) then
self.ball:LoadParticleEffect(-1, self.pFX_grass, {}); --grass fx
end
end
if (self.bBallInSand and self.clubNum ~= 12) then --sand fx on any non-putt in a sand bunker
self.ball:LoadParticleEffect(-1, self.pFX_sand, {});
end
self.strokeCount = self.strokeCount + 1;
HUD.SetFlashVariable(self.Hud.infoDisplay,"info1_strokes",self.strokeCount);
self.bAddedDownImp = false;
self.bSetBallMvmntCheck = false;
self.bNoBallMvmnt = false;
self:SetTimer(self.SHOTLIMIT_TIMERID, 15000); --protection for shot lasting too long
end,
OnUpdate = function(self,time)
local posOrig = g_Vectors.temp_v1;
if ((not self.bAddedDownImp) and (DistanceSqVectors2d(self.vHolePos, self.ball:GetWorldPos()) < sqr(self.holeSize))) then
self.ball:AddImpulse(-1, g_Vectors.v000, g_Vectors.down, 3); --downward impulse
self.bAddedDownImp = true;
end
if (self.ball:GetSpeed()== 0) then
CopyVector(posOrig, self.vLastBallPos);
CopyVector(self.vPosLand, self.ball:GetWorldPos());
self.distFromHole = math.sqrt(DistanceSqVectors2d(self.vHolePos, self.vPosLand));
if (posOrig.x ~= self.vPosLand.x or posOrig.y ~= self.vPosLand.y) then
self:GotoState("WaitForBallToSettle"); --ball is stopped
else --safeguard for soft putt on tee
if (not self.bSetBallMvmntCheck) then
self:SetTimer(self.BALLMVMNT_TIMERID, 1000);
self.bSetBallMvmntCheck = true;
else
self.bNoBallMvmnt = true; --2nd update ensures 1st speed=0 check was not before takeoff
end
end
end
if (not self.player:IsAnimationRunning(0,0)) then
local animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.idleStand;
self.player:StartAnimationAR(animFile, {blendTime=1, speed=1, loop=true});
end
end,
OnTimer = function(self, timerId)
if (timerId == self.BALLMVMNT_TIMERID) then
if (self.bNoBallMvmnt) then
self:GotoState("WaitForBallToSettle");
end
elseif (timerId == self.SHOTLIMIT_TIMERID) then
local ballPos = g_Vectors.temp_v2;
local ballVelDir = g_Vectors.temp_v3;
CopyVector(ballPos, self.ball:GetWorldPos());
CopyVector(ballVelDir, self.ball:GetVelocity());
local z = System.GetTerrainElevation(ballPos);
ballPos.z = z + .1;
self.ball:SetWorldPos(ballPos);
self.ball:AddImpulse(-1, g_Vectors.v000, ballVelDir, 2);
end
end,
OnEndState = function(self)
self:KillTimer(self.BALLMVMNT_TIMERID);
self:KillTimer(self.SHOTLIMIT_TIMERID);
end,
}
function ARGolfGame_SP:ShootBall()
if (not self.bUsingBallGreen or self.strokeCount == 0) then
self.ball:SetWorldPos(self.vBallPos);
end
self.ball:AddImpulse(-1, g_Vectors.v000, self.vShootImpulseDir, self.shootImpulsePow*1.3);
if (self.shootImpulsePow > 10) then
return true; --add particle trail
else
return false;
end
end
We shoot the ball with the Entity AddImpulse function after placing it at the nudged up position (calculated in the PerformShot state, a safeguard for the ball's interaction with the tee or terrain). According to the magnitude of the impulse, we return whether to start the particle effect trail.
WaitForBallToSettle
In this state we stop the particle effect trail with Entity FreeSlot and add a short delay of time for the ball to settle (if zero speed is reported before being completely at rest), then we go to the ShotComplete state.
ARGolfGame_SP.WaitForBallToSettle =
{
OnBeginState = function(self)
self.ball:FreeSlot(self.particleSlot);
if (self.bBallOnGreen) then
self:SetTimer(self.BALLSETTLE_TIMERID, 600);
else
self:SetTimer(self.BALLSETTLE_TIMERID, 400);
end
end,
OnTimer = function(self, timerId)
if (timerId == self.BALLSETTLE_TIMERID) then
self:GotoState("ShotComplete");
end
end,
}
ShotComplete
On entry, this state uses information about where the ball has landed to determine the next state. If in the hole, go to the HoleComplete state. If out of bounds, reset to the PrepareToShoot state. Otherwise, we check if on the green or in a sand bunker, and go to the Walking state.
ARGolfGame_SP.ShotComplete =
{
OnBeginState = function(self)
self.ball:EnablePhysics(false);
CopyVector(self.vPosLand, self.ball:GetWorldPos());
self.distFromHole = math.sqrt(DistanceSqVectors2d(self.vHolePos, self.vPosLand));
--local distTrav = math.sqrt(DistanceSqVectors2d(self.vPosLand, self.vLastBallPos)); local yards = distTrav * 1.094;
if (self.distFromHole < self.holeSize and self.vPosLand.z < self.vHolePos.z) then --in hole
self:GotoState("HoleComplete");
elseif (self:IsBallOutOfBounds(self.holeNum)) then --includes on-course water & defined oob area
self:ResetBallFromOutOfBounds(); --sends to state PrepareToShoot
self.strokeCount = self.strokeCount + 1; --penalty stroke
HUD.SetFlashVariable(self.Hud.infoDisplay,"info1_strokes",self.strokeCount);
else
if (self:IsBallInZone(self.holeNum)) then
if (self.bBallInSand) then
ARDebugMessage("Hazard Warning! Sand Bunker");
end
end
local groundZ = System.GetTerrainElevation(self.vPosLand);
if (math.abs(self.vPosLand.z - groundZ) > 2) then
self.vPosLand.z = groundZ + .05; --reset on terrain; protection for unlikely ball underground case
self.ball:SetWorldPos(self.vPosLand);
CopyVector(self.vLastBallPos, self.vPosLand);
else
CopyVector(self.vLastBallPos, self.vPosLand);
end
if (self.bBallOnTee) then
self.bBallOnTee = false;
end
self:GotoState("Walking");
end
end,
OnEndState = function(self)
HUD.SetFlashVariable(self.Hud.infoDisplay,"info1_bottom",""); --clear ReportShotAccuracy
end,
}
function ARGolfGame_SP:IsBallOutOfBounds(holeNum)
local bOut = false;
if (not self.svEnt:IsEntityInsideArea(self.Course.Hole[holeNum].Area[1].areaId, self.ball.id)) then
ARDebugMessage("Out of bounds! Please take your shot again");
bOut = true;
else
for i = 2, #(self.Course.Hole[holeNum].Area) do --check interior hazard areas
local b = self.svEnt:IsEntityInsideArea(self.Course.Hole[holeNum].Area[i].areaId, self.ball.id);
if (b and self.Course.Hole[holeNum].Area[i].bWater) then --water hazard
bOut = true; ARDebugMessage("Water Hazard! Please take your shot again");
elseif (b) then
bOut = true; ARDebugMessage("Hazard");
end
end
end
return bOut;
end
We call the Entity IsEntityInsideArea function to check if the ball is within areas linked to the golf supervisor entity placed in the level. In the Editor, the shape area entity is placed (RollupBar>Objects>Area>Shape) and linked by "Picking" the target entity in the Shape Parameters. These areas are in the Course.Hole.Area table, in which the first area listed is the hole boundary and subsequent areas are interior hazards, like a pond. If the ball is outside the hole boundary or inside an interior hazard we reset:
function ARGolfGame_SP:ResetBallFromOutOfBounds()
self.ball:SetWorldPos(self.vLastBallPos);
self.player:SetWorldPos(self.vTgtPos);
self.player:SetDirectionVector(self.vBallToHole);
self.distFromHole = math.sqrt(DistanceSqVectors2d(self.vHolePos, self.vLastBallPos));
self:GotoState("PrepareToShoot");
end
ResetBallFromOutOfBounds() resets the ball and the player, facing the hole, and transitions to the PrepareToShoot state so the player can take the shot again.
function ARGolfGame_SP:IsBallInZone(holeNum)
self.bBallInSand = false;
self.bBallOnGreen = false;
local bInAnyZone = false;
if (self.svEnt:IsEntityInsideArea(self.Course.Hole[holeNum].areaId_green, self.ball.id)) then
self.bBallOnGreen = true;
bInAnyZone = true;
elseif (self.svEnt:IsEntityInsideArea(self.Course.Hole[holeNum].areaId_bunker, self.ball.id)) then
self.bBallInSand = true;
bInAnyZone = true;
end
return bInAnyZone;
end
We use the same method to check whether the ball is in a "green" or "bunker" area (if the hole has multiple bunker areas, they share the same area id so only one IsEntityInsideArea check needs to be performed). If on the green or in a sand bunker, we store this info for the next shot, notify the player, and transition to the Walking state.
Walking
When this state is entered,
- The position of the target stance at the ball is calculated and the player is sent walking to that position with an AI signal, described below.
- If the distance to walk is far enough, the camera starts following (moving smoothly and continually looking at the player from a specified distance while the user can control the camera direction), and
- A 3-second timer delay is set, upon which the player will automatically skip to the target position.
- The Skip button is also displayed; receipt of the skip fscommand moves the player immediately to the target position with BeamPlayerToBall(), described below.
ARGolfGame_SP.Walking =
{
OnBeginState = function(self)
CopyVector(self.vBallPos, self.vLastBallPos);
CopyVector(self.vTgtPos, self:FindTgtPos(self.vBallPos));
self.bEndMove = false;
self:SendGotoSignal(self.vTgtPos);
local walkDistSq = DistanceSqVectors2d(self.vBallPos, self.player:GetWorldPos());
if (walkDistSq > 144 and self.Properties.bAutoSkipWalk ~= 0) then
self:SetTimer(self.SKIPWALK_TIMERID, 3000);
end
if (walkDistSq > 25) then
self.cam:Follow(self.player);
end
self.bDoSkipToBall = false;
self.bSkippedToBall = false;
self:ShowSkip();
end,
OnFSCommand = function(self,command,arg)
if (command == self.FSCommands.skip) then
self.bDoSkipToBall = true;
elseif (command == self.FSCommands.exit) then
self:ShowPopupConfirmExit();
elseif (command == self.FSCommands.popup_left) then
self:HidePopup();
self:GotoState("Finish_ReturnToGolfMatching");
elseif (command == self.FSCommands.popup_right or command == self.FSCommands.popup_x) then
self:HidePopup();
end
end,
OnUpdate = function(self,time)
local vWalkToTgt = g_Vectors.temp_v2;
SubVectors(vWalkToTgt, self.vTgtPos, self.player:GetWorldPos());
if (self.bEndMove) then
if (not self.bSkippedToBall) then
self.bSkippedToBall = true;
end
self.player:SetDirectionVector(self.vBallToHole);
self:GotoState("PrepareToShoot");
end
if (self.bDoSkipToBall and not self.bSkippedToBall) then
self:BeamPlayerToBall();
self.bSkippedToBall = true;
self:SetTimer(self.BEAMSAFEGUARD_TIMERID, 1000);
end
if (DistanceSqVectors2d(self.vHolePos, self.player:GetWorldPos()) < 4) then
if (not self.holeFlag:IsHidden()) then self.holeFlag:Hide(1) end;
end
end,
OnTimer = function(self, timerId)
if (timerId == self.SKIPWALK_TIMERID) then --auto-skip walk
self.bDoSkipToBall = true;
elseif (timerId == self.BEAMSAFEGUARD_TIMERID) then
self.player:SetDirectionVector(self.vBallToHole);
self:GotoState("PrepareToShoot");
end
end,
...
The OnUpdate callback transitions to the PrepareToShoot state when a flag is set (bEndMove) by a player callback (OnEndMove or OnCancelMove) when the target position is reached; it also hides the golf flag when the player is near the flag.
function ARGolfGame_SP:SendGotoSignal(tgtPos) local signalData = g_SignalData; CopyVector(signalData.point, tgtPos); signalData.point2.x = 0; signalData.fValue = 0; signalData.iValue = 0; AI.Signal(0, 10, "ACT_GOTO", self.player.id, signalData); end
The AI Signal tells the player to go to the specified point, and the path-finding will respect Forbidden Boundaries and Areas set up in the level (RollupBar>Objects>AI>ForbiddenBoundary / ForbiddenArea). In the golf level, a Forbidden Boundary (yellow) encompasses the boundary of the golf course (an avatar will not cross this line); a Forbidden Area (red) is used around a pond to prevent an avatar from entering the area.
function ARGolfGame_SP:BeamPlayerToBall() AI.Signal(0, 1, "CANCEL_CURRENT", self.player.id); self.player:SetWorldPos(self.vTgtPos); self.player:SetDirectionVector(self.vBallToHole); end
When we "beam" a player to the target position, the AI signal is canceled. However, this may not be done immediately, so we overwrite the player's direction for a few updates in the PrepareToShoot state (see tempUpdatesDirection).
...
OnEndState = function(self)
self:KillTimer(self.SKIPWALK_TIMERID);
self:KillTimer(self.BEAMSAFEGUARD_TIMERID);
self:HideSkip();
end,
}
When we exit the state, we deactivate the timer with Entity KillTimer, and hide the Skip button.
"Gimme" Putt
The "Gimme" automatic putt mimics the state transitions Perform Shot > Swing > Shoot > Shot Complete.
Perform Gimme
ARGolfGame_SP.PerformGimme =
{
OnBeginState = function(self)
if (not self.bUsingBallGreen) then
if (self.bCgfSwitching) then self:SwapBallGreen(true) end
end
HUD.SetFlashVariable(self.Hud.infoDisplay,"info1_bottom","'Gimme' Putt");
end,
OnTimer = function(self, timerId)
if (timerId == self.BLENDED_TIMERID) then
self:GotoState("GimmeSwing");
end
end,
OnUpdate = function(self,time)
if (self.tempUpdatesDirection < 5) then
self.player:SetDirectionVector(self.vBallToHole);
if (self.tempUpdatesDirection == 4) then
local animFile = "levels/ar/common/animations/human/"..self.player:GetAvatarBaseModel().."/minigames/golf/"..self.Anims.swingPutt_soft;
self.player:StartAnimationAR(animFile, {blendTime=.50, speed=1, loop=false});
self:SetTimer(self.BLENDED_TIMERID, 750);
end
self.tempUpdatesDirection = self.tempUpdatesDirection + 1;
end
end,
}
Gimme Swing
ARGolfGame_SP.GimmeSwing =
{
OnUpdate = function(self,time)
self.ball:EnablePhysics(true);
local animTime = 0;
if (self.player:IsAnimationRunning(0,0)) then
animTime = self.player:GetAnimationTime(0,0);
if (animTime > (self.timingPutt - .03)) then
Sound.Play(self.Sounds.swingPutt, self.vPlayerPos, SOUND_2D, SOUND_SEMANTIC_AMBIENCE_ONESHOT);
self:GotoState("GimmeShoot");
end
end
end,
}
Gimme Shoot
Instead of adding one impulse, like in the Shoot state, GimmeShoot simulates the putt by adding small impulses in the OnUpdate callback toward the hole using Entity AddImpulse, GetSpeed and GetVelocity functions.
ARGolfGame_SP.GimmeShoot =
{
OnBeginState = function(self)
self.strokeCount = self.strokeCount + 1;
HUD.SetFlashVariable(self.Hud.infoDisplay,"info1_strokes",self.strokeCount);
self.bSetGimmeTimer = false;
end,
OnUpdate = function(self,time)
local vBallPos = g_Vectors.temp_v1;
local vHoleDir = g_Vectors.temp_v2;
local vPushDir = g_Vectors.temp_v3;
CopyVector(vBallPos, self.ball:GetPos());
CopyVector(vHoleDir, DifferenceVectors(self.vHolePos, vBallPos));
local distFromHole = math.sqrt(DistanceSqVectors2d(self.vHolePos, vBallPos));
local appliedPow = clamp(distFromHole, .5, 2);
local vTerrNorm = g_Vectors.temp_v4;
CopyVector(vTerrNorm, System.GetTerrainSurfaceNormal(vBallPos));
local theDot = dotproduct3d(vTerrNorm, g_Vectors.up);
if (theDot < 0.996) then --enough of a slope to require taking the uphill direction into account
crossproduct3d(vPushDir, vTerrNorm, vHoleDir); --assuming terrain closely matches green mesh
NormalizeVector(vHoleDir);
FastScaleVector(vPushDir, NormalizeVector(vPushDir), appliedPow/15);
if (vPushDir.z > 0) then
FastSumVectors(vHoleDir, vHoleDir, vPushDir);
elseif (vPushDir.z < 0) then
FastDifferenceVectors(vHoleDir, vHoleDir, vPushDir);
end
end
vHoleDir.z = 0;
if (distFromHole < self.holeSize - .01) then
self.ball:SetWorldPos(SumVectors(self.ball:GetWorldPos(), ScaleVector(g_Vectors.down, .02)));
self.ball:AddImpulse(-1, g_Vectors.v000, g_Vectors.down, 2);
self:GotoState("GimmeComplete");
elseif (distFromHole < self.holeSize*2) then
FastDifferenceVectors(vHoleDir, vHoleDir, self.ball:GetVelocity())
self.ball:AddImpulse(-1, nil, vHoleDir, appliedPow);
if (not self.bSetGimmeTimer) then
self:SetTimer(self.GIMMESTOP_TIMERID, 2000); --safeguard against ball circling hole
self.bSetGimmeTimer = true;
end
else
if (self.ball:GetSpeed() < 2.5) then
self.ball:AddImpulse(-1, nil, vHoleDir, appliedPow);
end
end
if (not self.player:IsAnimationRunning(0,0)) then
local animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.idleStand;
self.player:StartAnimationAR(animFile, {blendTime=1, speed=1, loop=true});
end
end,
OnTimer = function(self,timerId)
if (timerId == self.GIMMESTOP_TIMERID) then
self.ball:SetWorldPos(SumVectors(self.vHolePos, ScaleVector(g_Vectors.down, .1)));
end
end,
OnEndState = function(self)
self.ball:SetWorldPos(SumVectors(self.vHolePos, ScaleVector(g_Vectors.down, .1))); --set the ball in the hole
end,
}
Gimme Complete
ARGolfGame_SP.GimmeComplete =
{
OnBeginState = function(self)
self:SetTimer(self.GIMMEINHOLE_TIMERID, 1000);
end,
OnTimer = function(self,timerId)
if (timerId == self.GIMMEINHOLE_TIMERID) then
self:GotoState("HoleComplete");
end
end,
OnUpdate = function(self,time)
if (not self.player:IsAnimationRunning(0,0)) then
local animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.idleStand;
self.player:StartAnimationAR(animFile, {blendTime=1, speed=1, loop=true});
end
end,
OnEndState = function(self)
HUD.SetFlashVariable(self.Hud.infoDisplay,"info1_bottom","");
end,
}
This state just adds a short delay before transitioning to the Hole Complete state, starts the idle animation if the putting animation has stopped, and clears the "Gimme Putt" text from the info display.
Hole Complete
We come to this state from either the ShotComplete or GimmeShoot state (or potentially from PrepareToShoot's safeguard). Score is stored, the clickable ARTeeSign map is deactivated, and we check if this is the last hole in the course shown in IsLastHole() below. If so, we transition to the CourseComplete state. Otherwise we:
- Play a reaction animation
- Set a timer to allow the reaction animation to play before going to the next state
- Initialize the next hole
- Transition to the next state: ShowHole if the course has per-hole sequences, else PrepareToShoot.
ARGolfGame_SP.HoleComplete =
{
OnBeginState = function(self)
if (self.tee_sign ~= nil) then
self.tee_sign:SetSelectableFromGolf(false);
end
self.holeScores[self.holeNum] = self.strokeCount;
self:ScoreNickname(self.holeNum);
self.strokeCount = 0;
if (self:IsLastHole()) then
self:GotoState("CourseComplete");
else
self.bUsingBallGreen = false;
local animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.reacNeutral;
self.player:StartAnimationAR(animFile, {blendTime=1, speed=1, loop=false});
self:SetTimer(self.ANIMPLAY_TIMERID, 3000);
end
end,
OnTimer = function(self,timerId)
if (timerId == self.ANIMPLAY_TIMERID) then
self:InitializeHole(self.holeNum);
if (self.Course.Seq.bCourseHasPerHoleSeqs) then
self:GotoState("ShowHole");
else
self:ShowInfoDisplay();
self:GotoState("PrepareToShoot");
end
end
end,
OnUpdate = function(self,time)
if (not self.player:IsAnimationRunning(0,0)) then
local animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.idleStand;
self.player:StartAnimationAR(animFile, {blendTime=1, speed=1, loop=true});
end
end,
}
function ARGolfGame_SP:IsLastHole()
local bLastHole = false;
if (self.holeNum == self.totalHoles) then
bLastHole = true;
else
self.holeNum = self.holeNum + 1;
end
return bLastHole;
end
Course Complete
On entering this state, the game is over. After we post our scores to the golf database, we display the scores and offer the choice to "Play Again" or "Exit Golf". We transition to the Finish_ReturnToGolfMatching state or the Finish_ExitGolf state according to user selection, and if no choice has been made at the end of the theme song, we transition to the Finish_ReturnToGolfMatching state.
ARGolfGame_SP.CourseComplete =
{
OnBeginState = function(self)
self:HideExit();
self:SetTimer(self.ENDCHOICE_TIMERID, 137000); --theme song length
self:GameOver();
self.soundId_end = Sound.PlayEx(self.Sounds.theme, self.vHolePos, SOUND_2D, .8, 0, 0, SOUND_SEMANTIC_HUD);
Sound.SetFadeTime(self.soundId_end, 1.0, 300);
...
Game Over
- In GameOver(), we:
- Play a reaction animation
- Play the end sequence with Movie PlaySequence, if it exists (our variable bCourseHasOpeningSeq signifies both an opening and end sequence)
- Set a timer upon which we check if scores have been posted to the golf database
- Then the ending song is smoothly started with Sound PlayEx and SetFadeTime
function ARGolfGame_SP:GameOver()
self.holeFlag:Hide(1);
local animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.reacNeutral;
self.player:StartAnimationAR(animFile, {blendTime=1, speed=1, loop=false});
if (self.Course.Seq.bCourseHasOpeningSeq) then
Movie.PlaySequence(self.Course.Seq.end_seq)
self:SetTimer(self.SHOWSCOREBOARD_TIMERID, 2500);
else
self:SetTimer(self.SHOWSCOREBOARD_TIMERID, 1000);
end
end
Submit scores to the Golf Database
... (OnBeginState continued)
--Post Scores
local scores = "";
local totalscore = 0;
if (not System.IsEditor()) then
for i = 1, self.totalHoles do
totalscore = totalscore + self.holeScores[i];
scores = scores.. self.info.ClientID ..",".. self.Course.Hole[i].actualHoleNum ..",".. self.holeScores[i];
if i ~= self.totalHoles then
scores = scores.. ",";
end
end
local params = {course = self.Course.sGameClass,
section = self.Course.section,
players = self.info.ClientID ..",".. self.info.Name,
mode = "singleplayer",
totalscores = self.info.ClientID ..",".. totalscore,
scores = scores};
ARSubmitPost(self.Properties.addgolfgame, self.Properties.addresults, params);
self.bPostedScore = false;
self:PrepareScores(totalscore); --for scoreboard
end
end,
...
- With ARSubmitPost, we post our scores to the golf database, which stores player and game information we can later query and display (details on the High_Score_Server page)
- Next, we call PrepareScores to set scoreboard variables with HUD SetFlashVariable:
function ARGolfGame_SP:PrepareScores(myTotal)
local totalCourseDist = 0;
local totalCoursePar = 0;
if (self.totalHoles == 3) then --3 hole ui:
HUD.SetFlashVariable(self.Hud.score3,"p1_name", "P1: "..self.info.Name);
for n = 1, self.totalHoles do
HUD.SetFlashVariable(self.Hud.score3,"hole".. n, self.Course.Hole[n].actualHoleNum);
HUD.SetFlashVariable(self.Hud.score3,"dist".. n, self.Course.Hole[n].distYd);
HUD.SetFlashVariable(self.Hud.score3,"par".. n, self.Course.Hole[n].par);
HUD.SetFlashVariable(self.Hud.score3,"p1_hole" .. n, self.holeScores[n]);
totalCourseDist = totalCourseDist + self.Course.Hole[n].distYd;
totalCoursePar = totalCoursePar + self.Course.Hole[n].par;
end
--totals: holeOut, distOut, parOut, p#_holeOut
HUD.SetFlashVariable(self.Hud.score3,"distOut", totalCourseDist);
HUD.SetFlashVariable(self.Hud.score3,"parOut", totalCoursePar);
HUD.SetFlashVariable(self.Hud.score3,"p1_holeOut", myTotal);
elseif (self.totalHoles == 1) then --1 hole ui:
HUD.SetFlashVariable(self.Hud.score1,"p1_name", "P1: "..self.info.Name);
HUD.SetFlashVariable(self.Hud.score1,"hole1", self.Course.Hole[1].actualHoleNum);
HUD.SetFlashVariable(self.Hud.score1,"dist1", self.Course.Hole[1].distYd);
HUD.SetFlashVariable(self.Hud.score1,"par1", self.Course.Hole[1].par);
HUD.SetFlashVariable(self.Hud.score1,"p1_hole1", self.holeScores[1]);
end
end
function ARGolfGame_SP:ShowScoreboard()
if (self.totalHoles == 1) then
HUD.SetFlashVariable(self.Hud.score1,"scoreboardMessage","Play Again or Exit?");
HUD.SetFlashVariable(self.Hud.score1,"btnLeft_Play","Play");
HUD.SetFlashVariable(self.Hud.score1,"btnRight_Exit","Exit");
HUD.ShowFlash(self.Hud.score1);
HUD.Modal(self.Hud.score1);
HUD.AddFSCommandListener(self.Hud.score1,self.id);
elseif (self.totalHoles == 3) then
HUD.SetFlashVariable(self.Hud.score3,"scoreboardMessage","Play Again or Exit?");
HUD.SetFlashVariable(self.Hud.score3,"btnLeft_Play","Play");
HUD.SetFlashVariable(self.Hud.score3,"btnRight_Exit","Exit");
HUD.ShowFlash(self.Hud.score3);
HUD.Modal(self.Hud.score3);
HUD.AddFSCommandListener(self.Hud.score3,self.id);
end
end
...
OnUpdate = function(self,time)
if Game.IsDownloading() then
System.Log("ARGolfGame_SP.CourseComplete Game.IsDownloading()");
else
if not self.bPostedScore then
self.bPostedScore = true;
end
end
if (not self.player:IsAnimationRunning(0,0)) then
local animFile = "levels/ar/common/animations/human/" .. self.player:GetAvatarBaseModel() .. "/minigames/golf/" .. self.Anims.idleStand;
self.player:StartAnimationAR(animFile, {blendTime=1, speed=1, loop=true});
end
end,
OnTimer = function(self,timerId)
if (timerId == self.ENDCHOICE_TIMERID) then --if no choice was made
self:GotoState("Finish_ReturnToGolfMatching");
elseif (timerId == self.SHOWSCOREBOARD_TIMERID) then
if (self.bPostedScore) then
self:HideInfoDisplay();
self:ShowScoreboard(); --only show after scores have been POSTED
else --restart timer again to check if bPostedScore
self:SetTimer(self.SHOWSCOREBOARD_TIMERID, 2000);
end
end
end,
...
Upon the ShowScoreboard timer, we check if scores have been posted to the golf database (flagged by Game.IsDownloading in OnUpdate ), and call HideInfoDisplay() and ShowScoreboard() to show scores and offer the options play again or exit golf (below).
...
OnFSCommand = function(self,command,arg)
if (command == self.FSCommands.popup_left) then
self:GotoState("Finish_ReturnToGolfMatching");
elseif (command == self.FSCommands.popup_right) then
self:GotoState("Finish_ExitGolf");
end
end,
OnEndState = function(self)
self:HidePopup();
Movie.StopAllSequences();
Sound.StopSound(self.soundId_end);
Sound.SetFadeTime(self.soundId_end, 0.0, 8000);
self:KillTimer(self.SHOWSCOREBOARD_TIMERID);
self:KillTimer(self.ENDCHOICE_TIMERID);
end,
}
In the OnFSCommand callback, we transition to the next state, Finish_ReturnToGolfMatching or Finish_ExitGolf, depending on the choice (the left button is labeled "Play" and sends the popup_left fscommand, and the right button is labeled "Exit" and sends the popup_right fscommand).
On exiting the state, we hide the Popup (also removing the fscommand listener), call Movie StopAllSequences, stop the sound and cleanup the timers.
Finish
Finish_Return To Golf Matching
This game-finished state cleans up by
- Clearing the info display in ClearHUD() with HUD SetFlashVariable
- If we are in the Editor, calling PlayAgain to restart the game (described below), otherwise:
- Disabling timers in CleanupTimers() with Entity KillTimer
- Disabling the golf action map with ActionMap_Manager EnableActionMap
- Re-enabling the standard action maps (if they were enabled at game start), in ReenableActionMaps() below
- Calling the Entity Activate function to disable the game entity (turning off OnUpdate callbacks)
- Calling CleanupAvatarAndCamera():
- Stopping the ARGolfCamera, restoring the original view which was active when we started the camera
- Deleting the camera
- Destroying character attachments in RemoveClubs(), shown below
- Deleting the player avatar
- Calling SvShowMatching(), defined by the golf supervisor, which causes the golf supervisor to exit (deleting this ARGolfGame_SP entity), go to its "Available" state, and call its GameStart() function, defined in Multiplayer_Minigame_API Game/Scripts/Utils/AR/ARMiniGameSupervisorBase.lua
ARGolfGame_SP.Finish_ReturnToGolfMatching =
{
OnBeginState = function(self)
self:ClearHUD();
self:HideExit();
if (System.IsEditor()) then
self:PlayAgain();
else
self:CleanupTimers();
ActionMapManager.EnableActionMap("ARGolf", false);
self:ReenableActionMaps();
self:Activate(0);
self:CleanupAvatarAndCamera();
self:SvShowMatching();
end
end,
}
function ARGolfGame_SP:ReenableActionMaps()
if (self.bAMapWasEnabled_Player) then
ActionMapManager.EnableActionMap("player",true);
end
if (self.bAMapWasEnabled_Default) then
ActionMapManager.EnableActionMap("default",true);
end
if (self.bAMapWasEnabled_Debug) then
ActionMapManager.EnableActionMap("debug",true);
end
end
function ARGolfGame_SP:CleanupAvatarAndCamera()
self.cam:GotoState("Free");
self.cam:Stop();
self.cam:DeleteThis();
self:RemoveClubs();
self.player:DeleteThis();
end
function ARGolfGame_SP:RemoveClubs()
for i = 1, #(self.Clubs) do
self.player:DestroyAttachment(0, self.Clubs[i].attachmentName);
end
end
function ARGolfGame_SP:PlayAgain()
self.holeNum = 1;
self:InitializeHole(self.holeNum);
self:GotoState("ShowCourse");
end
If we are in the Editor (System IsEditor is true), PlayAgain() is called and we initialize the first hole and proceed to the ShowCourse state to restart the game.
Finish_Exit Golf
ARGolfGame_SP.Finish_ExitGolf =
{
OnBeginState = function(self)
self:ClearHUD();
self:HideExit();
self:CleanupTimers();
ActionMapManager.EnableActionMap("ARGolf", false);
self:ReenableActionMaps();
self:Activate(0);
if (not System.IsEditor()) then
self:SvExitGolf();
end
end,
}
This state does most of the same cleanup as Finish_ReturnToGolfMatching, and calls the SvExitGolf() function which, like SvShowMatching(), causes the golf supervisor to exit (deleting this ARGolfGame_SP entity), go to its "Available" state, but instead of restarting, it calls CryAction ScheduleEndLevel to exit the golf level and go to the level specified in the golf supervisor's exitToLevel property ("AR_Startup").
Next, check out the Multiplayer_Golf example which builds on this single player version and uses the Multiplayer_Minigame_API, broadcast messages, mouse-click navigation, and chat while a party goes golfing together.





