Golf

From Blue Mars Developer Guidebook

Jump to: navigation, search
There are security restrictions on this article

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.

Image:golfstates_sp.png

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

Image:Start.jpg

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
  1. Call the Entity Activate function to ensure that the script's OnUpdate callbacks are invoked every frame
  2. Call the ARDebug function to turn on the lua debugger and set the log verbosity level (during development)
  3. Load the Golf Action Map with the ActionMap_Manager function LoadFromXML, if it is not already loaded
  4. Turn off the debug display info (top right corner of screen) with a Console Variable (during development)
  5. 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:

  1. Override the SinglePlayerCallback with golf-specific functionality
  2. Spawn the single player entity (ARGolfGame_SP class) with System SpawnEntity
  3. Define the SvShowMatching and SvExitGolf functions used by the Finish states below
  4. Define the DressUp function in PrepareSinglePlayerEntity() which grabs the avatar customizations
  5. Deliver the Course Info, specified in the supervisor's properties, to the single player entity:
    1. Use Script ReloadScript and loadstring to get the Course table in the course info file, which contains course-specific data.
    2. 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

Image:Showcourse.jpg

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,
}
  1. 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:
  2. Play the opening sequence (a preview of the golf course, created with the Editor's TrackView) with Movie PlaySequence.
  3. Play sound with Sound PlayEx, and use SetFadeTime to prevent popping or clicking.
  4. Display the Skip button (described below).
  5. 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
  1. We display the Skip button with the HUD ShowFlash function.
  2. HandleFlashEvents causes the movie to receive mouse events.
  3. 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

Image:golfing.jpg

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:

  1. The "main" HUD is displayed in ShowMain(), with HUD ShowFlash, HandleFlashEvents and AddFSCommandListener, as in ShowSkip() above.
  2. A default club is chosen in FindProperClub(), based on distance to the hole.
  3. 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().
  4. The HUD RegisterActionCallback function is called to direct actions to this state's OnAction callback.
  5. 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:

  1. Right-click-drag mouse actions rotate the player's aim direction
    1. The right-click-down action enables aiming and we get the current location of the mouse with System GetHardwareMouseX.
    2. 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.
    3. Aiming is disabled when the right-click-up action is received.
  2. Up/Down arrow actions temporarily raise/lower the camera for perspective.
    1. While raised or lowered, we hide the "main" Flash movie.
    2. 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:

  1. Load the proper ball, depending on whether we are putting, with SwapBallGreen(), described below.
  2. Nudge the ball above the terrain with NewBallPos().
  3. Blend into the proper swing animation, depending on the club category and swing power, with Entity StartAnimationAR.
  4. 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).

  1. Entity FreeSlot removes the current cgf in the slot.
  2. Entity LoadObject loads the new cgf.
  3. Call Entity Physicalize and SetPhysicParams to Physicalize and set simulation properties.
  4. 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

Image:Walktoposition.jpg

When this state is entered,

  1. 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.
  2. 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
  3. A 3-second timer delay is set, upon which the player will automatically skip to the target position.
  4. 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:

  1. Play a reaction animation
  2. Set a timer to allow the reaction animation to play before going to the next state
  3. Initialize the next hole
  4. 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

  1. In GameOver(), we:
    1. Play a reaction animation
    2. Play the end sequence with Movie PlaySequence, if it exists (our variable bCourseHasOpeningSeq signifies both an opening and end sequence)
    3. Set a timer upon which we check if scores have been posted to the golf database
  2. 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,
  ...
  1. 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)
  2. 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

  1. Clearing the info display in ClearHUD() with HUD SetFlashVariable
  2. If we are in the Editor, calling PlayAgain to restart the game (described below), otherwise:
  3. Disabling timers in CleanupTimers() with Entity KillTimer
  4. Disabling the golf action map with ActionMap_Manager EnableActionMap
  5. Re-enabling the standard action maps (if they were enabled at game start), in ReenableActionMaps() below
  6. Calling the Entity Activate function to disable the game entity (turning off OnUpdate callbacks)
  7. Calling CleanupAvatarAndCamera():
    1. Stopping the ARGolfCamera, restoring the original view which was active when we started the camera
    2. Deleting the camera
    3. Destroying character attachments in RemoveClubs(), shown below
    4. Deleting the player avatar
  8. 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.

Problems with this wiki page? Contact us either by: Support Email or Support Ticket System

Blue Mars Guidebook Privacy Policy
Blue Mars Guidebook Community Guidelines

Personal tools