Multiplayer Golf

From Blue Mars Developer Guidebook

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

Contents

Overview

This page expands on the single-player Golf example and shows how a multiplayer game is set up with Multiplayer_Minigame_API.

Reconstructing Golf

State Machines

The Multiplayer Overview gives us an idea of the separation of duties. In the multiplayer golf game, the game master is basically responsible for:

  1. tracking the score of each player
  2. determining who's turn it is
  3. recognizing when the game is over

Image:golfmasterstates.jpg

The player script resembles the single-player golf script, except now we have to account for the period when it's not this player's turn. We use a Golfer table to store each player's avatar entity, info, strokes, and other navigation and shot stats (we store the current turn player's number and refer to that avatar as self.Golfer[self.currentTurn].player). When it is a player's turn, as in the single-player game, we:

  1. walk (or skip) to the ball
  2. prepare to swing (choose a club, aim, shot power)
  3. swing
  4. wait for the ball to stop
  5. if the ball is in the hole, either start afresh for the next hole, or exit/restart game

When it's not a player's turn, we:

  1. walk (or skip) to the ball (but to a spectator position)
  2. if the current-turn player is already there, skip/beam there
  3. watch the shot in progress, with the ability to move and look around
  4. repeat until it's your turn

The player state machine is similar to the single-player game but with a parallel branch for the player waiting his turn, which looks something like:

Image:golfgamempstates.jpg

Initialization

The mechanics for starting the game are implemented in the Golf Supervisor entity -- see the diagram in Initialization. The golf "supervisor" entity, set to AutoStart, calls its GameStart function after loading the golf level, which invokes the Multiplayer InitializeAndJoin function. In multiplayer mode, a user may Create or Join a game. When the game-creator starts the game, the client receives the JoinCallback and spawns either a multiplayer entity and a game master entity (created game), or just a multiplayer entity (joined game).

We override the JoinCallback in the supervisor base class with golf-specific functionality:

function ARGolfSupervisor:JoinCallback(numclients, roominfo)
  self:GetCourseInfo(roominfo.section);
  if (self.MP_IsMaster) then
    local ent = System.SpawnEntity {class = self.Properties.sMasterEntityClass,
                                    name = self:GetName() .. "_master",
                                    position = self:GetPos(),
                                   }
    self:PrepareMasterEntity (ent, roominfo);
    ent:SetCourseInfo(self.Course, self);
    ent:GotoState (ent.States[1])
  end

  local ent = System.SpawnEntity {class = self.Properties.sPlayerEntityClass,
                                  name = self:GetName() .. "_player",
                                  position = self:GetPos(),
                                 }
  self:PreparePlayerEntity(ent, roominfo);
  ent:SetCourseInfo(self.Course, self);
  ent.SvShowMatching = function()
    self:Exit();
    self:GotoState("Available");
    self:GameStart();
  end
  ent.SvExitGolf = function()
      self:Exit();
      self:GotoState("Available");
      CryAction.ScheduleEndLevel(self.Properties.sExitToLevel);
  end
  ent:GotoState (ent.States[1])
end
  1. Grab course-specific data with GetCourseInfo(), described in Golf Supervisor
  2. If my client created the game, spawn the master entity (ARGolfGame_Master) and
  3. Spawn the player entity (ARGolfGame_MP class) with System SpawnEntity
  4. Call PrepareMasterEntity and PreparePlayerEntity to prepare the master and player entities with the functions described in Multiplayer_Minigame_API
    1. Set up the PlayerStatus (master), SharedData (player), and RoomData tables
    2. Define functions like GetPlayerStatus (master), UpdateStatus (player), GetPlayerCount, Broadcast, DressUp (grabs avatar customizations), etc.
  5. Define the SvShowMatching and SvExitGolf functions used in finishing the game
  6. Call SetCourseInfo() to deliver the Course Info to the master and player entities. The master's SetCourseInfo() also calls SetGameVars(), below, which illustrates the Multiplayer_Minigame_API GetPlayerCount function (the number of players in the RoomData table), and initializes the score tables:
function ARGolfGame_Master:SetGameVars()
  self.partySize = self:GetPlayerCount();
  self.holeNum = 1;
  for i,v in ipairs (self.RoomData.players) do
    self.Golfer[i] = {};
    self.Golfer[i].ClientID = v;
    if (svEnt) then
      self.Golfer[i].Name = ARMultiPlayerMiniGame.GetAvatarData(svEnt, self.Golfer[i].ClientID).ShortName;
      System.Log("ARGolfGame_Master:SetGameVars got short name: ".. self.Golfer[i].Name);
    end
    self.Golfer[i].holeScores = {};
    self.Golfer[i].totalScore = 0;
    for n = 1, self.totalHoles do
      self.Golfer[i].holeScores[n] = 0;
    end
  end
  self:ResetVarsPerHole();
end

Communication

As described in the Game_Entities_API page (also see the diagrams in Message Passing), the Master can send a message to players and retrieve data sent by players:

1. Master sends two types of SendEvent messages:
  • updateSharedData modifies the players' SharedData table
  • newState forces a player state transition
2. Master queries/clears the PlayerStatus table
  • GetPlayerStatus queries a player's data in the PlayerStatus table
  • ClearState clears the value of a specific element for each player
3. At the end of the game, the Master posts all player scores to the golf database with ARSubmitPost, described on the High_Score_Server page, as shown in the single player example.

The Player can send a message to the master or to other players, and retrieve data set by the master:

1. Player sends two types of messages:
  • UpdateStatus to update the player's data in the master's PlayerStatus table
  • Broadcast an event directly to other players
2. Player data receipt
  • Query its SharedData table, updated by the master
  • Receive Broadcast data with the OnBroadcast callback

Example Code

Let's look at some examples of communication between the master and player entities.

State transition

The master is organized with several states that parallel the key points in the player state sequence, used for newState transitions. This sequence illustrates the master waiting until all players are ready to move on to a new state.

ARGolfGame_Master =
{
  States = 
  {
    "Init",
    "Start",
    "ShowCourse",
    "ShowHole",
    "DetermineNextTurn",
    "Walking",
    "PrepareToShoot",
    "PerformShot",
    "PerformGimme",
    "ShotComplete",
    "OutOfBounds",  
    "ReactionBallInHole",
    "HoleComplete",
    "CourseComplete",
    "Finish",
    "EarlyExit",
    "APlayerLeft",
  },
  ...
ARGolfGame_MP = 
{
  States =
  {
    "Init",
    "Start",
    "ShowCourse",
    "ShowHole",
    "Walking",
    "PrepareToShoot",
    "WatchPlayer",
    "PerformShot",
    "Swing",
    "Shoot",
    "WaitForBallToSettle",
    "ShotComplete",    
    "PerformGimme",
    "GimmeSwing",
    "GimmeShoot",
    "GimmeComplete",
    "OutOfBounds",
    "ReactionBallInHole",
    "HoleComplete",
    "CourseComplete",
    "Finish_ReturnToGolfMatching",
    "Finish_ExitGolf",
    "MasterLeft",
    "EarlyExit",
  },
  ...
1. Upon starting the game, the supervisor sends both the master and the player to their first state, Init. Each player sends a message with UpdateStatus to notify the master when its Init state is complete. This sets the value for this player's Done_Init key to true in the PlayerStatus table.
self:UpdateStatus("Done_Init", true);
2. In the master's Init state, it performs the Multiplayer_Minigame_API StateAllTrue check OnUpdate, to find out when the Done_Init key is true for each player. Then it clears the Done_Init key for each player with ClearState. (Similarly, if any player chose to exit the game, setting the Exit key to true in the PlayerStatus table, the master responds to Multiplayer_Minigame_API StateAnyTrue by clearing the Exit key and transitioning to the EarlyExit state.)
ARGolfGame_Master.Init =
{
  OnUpdate = function(self,time)   
    if self:StateAllTrue("Done_Init") then
      self:ClearState("Done_Init");
      for i = 1, self.partySize do
        self:SendEvent {updateSharedData = {["HoleScores_".. i] = self.Golfer[i].holeScores}}
        self:SendEvent {updateSharedData = {["TotalScore_".. i] = 0}} 
      end
      self:GotoState("Start");
    elseif (self:StateAnyTrue("Exit")) then
      self:ClearState("Exit");
      self:GotoState("EarlyExit");  
    end
    end 
  end,
}
3. The master uses SendEvent type updateSharedData to initialize scores in the SharedData table (i.e., key: "HoleScores_1", value: Player 1's score table)
4. Then the master transitions to its Start state, in which it sends the players to their Start states with SendEvent type newState:
ARGolfGame_Master.Start =
{
  OnBeginState = function(self) 
    self:SendEvent {newState = "Start"}
  end,
  ...

By updating the SharedData and transitioning to newStates, we can ensure that the player has received the updated shared data upon entering the new state.

Broadcast

An example of player-Broadcast events is when the current-turn player changes aim direction or club selection. From the ARGolfGame_MP.PrepareToShoot state, this player sends:

self:Broadcast({event = "clubupdate", club = self.clubNum});
self:Broadcast({event = "aimupdate", pos = vNewPlayerPos, ang = self.vNewPlayerAngle});

Players receive the OnBroadcast callback, defined in both the PrepareToShoot and WatchPlayer states; the data is used if the bCanUpdatePos flag allows (this prevents interference from a player's own broadcasts):

OnBroadcast = function (self, data)
  if (self.bCanUpdatePos) then
    if (data.event == "aimupdate") then
      self.Golfer[self.currentTurn].player:SetWorldPos(data.pos);
      self.Golfer[self.currentTurn].player:SetWorldAngles(data.ang);
    elseif (data.event == "clubupdate") then
      self:SetClub(data.club, false);
    end
  end 
  --update a non-current plyr navigating:
  if (data.event == "navupdate") then
    self:SendGotoSignal(data.plyrNum, data.pos);
    self.Golfer[data.plyrNum].bWatchNavigatingToPoint = true;
  end
end,

We update the players' position and angles this way since the golf scripts handle the spawning and sychronization of avatars. The golf level does not contain an ARDefaultCamera entity which automatically spawns an avatar and starts the standard "city" synchronization of the avatar's position, etc.


Each player can receive a navupdate from a non-current player moving around in the WatchPlayer state, discussed in the Navigation section below.

Sending and Receiving PerformShot data

After selecting power and aiming in ARGolfGame_MP.PrepareToShoot, the current player clicks the "Shoot" button and we use UpdateStatus to send:

  1. ShotInfoTakeoff settings to the master's PlayerStatus table, along with the
  2. PlayerPosDir (player's final position and direction)
  3. Done_PrepareToShoot status to signal that it is time to perform the shot
ARGolfGame_MP.PrepareToShoot = 
{
  OnFSCommand = function(self,command,arg)
    ...
    if (command == self.FSCommands.shoot) then
      ...(calculation of shot settings)
      self.myShotInfoTakeoff = {impulsePow = impulsePow, 
                                impulseDir = impulseDir,
                                clubNum = self.clubNum, 
                                shotInaccuracy = angOff,
                                powPenalty = powPenalty,
                               }
      local myPlayerInfo = {plyrPos = self.player:GetPos(), 
                            plyrDir = self.player:GetDirectionVector(),
                           }
      self:UpdateStatus("ShotInfoTakeoff", self.myShotInfoTakeoff);
      self:UpdateStatus("PlayerPosDir", myPlayerInfo);
      self:UpdateStatus("Done_PrepareToShoot", true);  
      ...

In the master's OnUpdate callback, the current player's Done_PrepareToShoot status is received, upon which we:

  1. Clear Done_PrepareToShoot with ClearState
  2. Update the players' SharedData tables with the ShotInfoTakeoff and PlayerPosDir data, received from the current player
  3. Go to the PerformShot state, below, which causes the players to transition to their PerformShot state. The master waits in this state until the shot is complete (ball stopped) or the ball is out-of-bounds.
ARGolfGame_Master.PrepareToShoot = 
{
  OnBeginState = function(self)
    local dist = 0;
    dist = self:GetPlayerStatus(self.currentTurn).ShotInfoLanded.distFromHole;
    if (dist ~= 0 and self:GetPlayerStatus(self.currentTurn).ShotInfoLanded.distFromHole < self.gimmePuttDist) then
      self:GotoState("PerformGimme");
    else
      self:SendEvent {newState = "PrepareToShoot"}
    end
  end,
  OnUpdate = function(self,time)
    if (self:GetPlayerStatus(self.currentTurn).Done_PrepareToShoot) then
      self:ClearState("Done_PrepareToShoot");
      self:SendEvent {updateSharedData = {["ShotInfoTakeoff_" .. self.currentTurn] = self:GetPlayerStatus(self.currentTurn).ShotInfoTakeoff}}
      self:SendEvent {updateSharedData = {["PlayerPosDir_" .. self.currentTurn] = self:GetPlayerStatus(self.currentTurn).PlayerPosDir}}
      self:GotoState("PerformShot");
      ...
ARGolfGame_Master.PerformShot = 
{
  OnBeginState = function(self)
    self:SendEvent {newState = "PerformShot"};
  end,
  OnUpdate = function(self,time)
    if (self:GetPlayerStatus(self.currentTurn).BallStopped) then
      self:ClearState("BallStopped");
      self:GotoState("ShotComplete");
    elseif (self:GetPlayerStatus(self.currentTurn).OutOfBounds) then
      self:ClearState("OutOfBounds");
      self:GotoState("OutOfBounds");
    ...
    end
  end,
}

In the players' PerformShot state, we load the shot data (shot impulse power and direction, etc) from the SharedData table which will be used to perform the shot. We also disable player collision to prevent other players from interfering with the shot.

ARGolfGame_MP.PerformShot =
{
  OnBeginState = function(self) 
    local turnData = self.SharedData["ShotInfoTakeoff_" .. self.currentTurn];
    local turnPlyrData = self.SharedData["PlayerPosDir_" .. self.currentTurn];   
    self.shootImpulsePow = turnData.impulsePow;
    CopyVector(self.vShootImpulseDir,turnData.impulseDir);
  
    for i=1, self.partySize do --turn off plyr collision
      self.Golfer[i].player:SetColliderMode(1); --1=disabled, 0=default
    end
    ...

Sending ShotComplete data

When the player's shot is complete, we check if the ball landed in-bounds or in a zone (described in single player Golf). Then we either:

  1. Notify the master if the ball is out-of-bounds with UpdateStatus OutOfBounds (upon which we count the stroke and go back to PrepareToShoot) or
  2. Update our ShotInfoLanded and notify the master with UpdateStatus BallStopped
ARGolfGame_MP.ShotComplete =
{
  OnBeginState = function(self)  
    for i=1, self.partySize do --turn on plyr collision
      self.Golfer[i].player:SetColliderMode(0); --0 default
    end
    self.ball:EnablePhysics(false);
    local bInHole = false;
    local bOutOfBounds = false;
    if (self.myPlayerNum == self.currentTurn) then
      CopyVector(self.vPosLand, self.ball:GetWorldPos());
      self.Golfer[self.currentTurn].distFromHole = math.sqrt(DistanceSqVectors2d(self.vHolePos, self.vPosLand));
      self.Golfer[self.currentTurn].distTravelled = math.sqrt(DistanceSqVectors2d(self.vPosLand, self.vPosOrig));
      if (self.Golfer[self.currentTurn].distFromHole < self.holeSize and self.vPosLand.z < self.vHolePos.z) then
        bInHole = true;
      elseif (self:IsBallOutOfBounds(self.holeNum)) then --check defined OOB areas
        self:UpdateStatus("OutOfBounds", true);
        bOutOfBounds = true;
      end
      if (not bOutOfBounds) then   
        if (not bInHole) then       
          if (not self:IsBallInZone(self.holeNum, self.currentTurn)) then  --checks area_zones
            self.Golfer[self.currentTurn].bBallInSand = false;
            self.Golfer[self.currentTurn].bOnGreen = false;
          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
        end
        if (self.Golfer[self.myPlayerNum].bBallOnTee) then
          self.Golfer[self.myPlayerNum].bBallOnTee = false;
        end
        self.myShotInfoLanded = {distFromHole = self.Golfer[self.currentTurn].distFromHole, 
                                 distTravelled = self.Golfer[self.currentTurn].distTravelled,
                                 posLand = self.vPosLand,
                                 bInHole = bInHole,
                                 bInSand = self.Golfer[self.currentTurn].bBallInSand,
                                 bOnGreen = self.Golfer[self.currentTurn].bBallOnGreen,
                                 bOnTee = self.Golfer[self.myPlayerNum].bBallOnTee,
                                }
        if (bInHole) then
        self:UpdateStatus("ShotInfoLanded", self.myShotInfoLanded);
        self:UpdateStatus("BallStopped", true);
      end
    end
  end,

Upon receiving BallStopped in the Master's PerformShot state, the master transitions to its ShotComplete state in which

  1. The SharedData tables are updated with the received ShotInfoLanded
  2. A stroke is added to the TrackGolfers holeScores table and passed on to the SharedData tables
  3. If the ball is in the hole, we call BallInHole() to go to the ReactionBallInHole state to play an animation before going to DetermineNextTurn
  4. Otherwise we go directly to the DetermineNextTurn state which will determine if the hole is completed by all players, and if not, which player has the next turn
ARGolfGame_Master.ShotComplete =
{
  OnBeginState = function(self)
    self:SendEvent {updateSharedData = {["ShotInfoLanded_" .. self.currentTurn] = self:GetPlayerStatus(self.currentTurn).ShotInfoLanded}}
    self:AddStroke(self.currentTurn, self.holeNum, 1); 
    self:SendEvent {updateSharedData = {["HoleScores_" .. self.currentTurn] = self.Golfer[self.currentTurn].holeScores}}      
    if (self:GetPlayerStatus(self.currentTurn).ShotInfoLanded.bInHole) then
      self:BallInHole();
    else 
      self:GotoState("DetermineNextTurn");
    end
  end,
}

Navigation

In the WatchPlayer state, a player is allowed to navigate (move and look around) while it is another player's turn. To reproduce the type of navigation done in a city, we have some Mouse Picking utility functions, like picking a point on terrain with ARMousePickTerrain. Here we use ARMousePickAnyPosExceptItem to pick any point that is not on a selectable item. This prevents a click on the ARTeeSign (a selectable tee sign which brings up a map) from also triggering navigation.

In ARGolfGame_MP.WatchPlayer's OnAction callback, we Broadcast the clicked point with some distance limitations:

OnAction = function(self, action, activationMode)                 
  if (action == self.Actions.mouseClick) then --navigate to point
    local pos, dist = ARMousePickAnyPosExceptItem();
    if (pos ~= nil and dist < 20) then --limit distance
      local distPlyr = DistanceSqVectors2d(self.player:GetWorldPos(), pos);
      if (distPlyr > 4 and distPlyr < 100) then --not closer than 2m or further than 10m from current player
        self:Broadcast({event = "navupdate", pos = pos, plyrNum = self.myPlayerNum});
      end
    end
  ...

OnBroadcast, each player, including the one who is navigating, calls SendGotoSignal(), described in Golf, to start walking. In PrepareToShoot, the OnBroadcast is as described above. In WatchPlayer's OnBroadcast, we have the added check to display the marker (bouncing cone) for the player who is navigating:

OnBroadcast = function (self, data)
  ...
  if (data.event == "navupdate") then
    self:SendGotoSignal(data.plyrNum, data.pos);
    if (data.plyrNum == self.myPlayerNum) then
      --show nav marker
      self.marker:SetPos(data.pos);
      self.marker:SetScale(0.25);
      self.marker:StartAnimationAR("Default", {layer=0, loop=true, forceUpdate=true});
      self.marker:Hide(0);
    end
    self.Golfer[data.plyrNum].bWatchNavigatingToPoint = true;
  end
end,
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