Game Logic

Will Gueble

2017/06/12

Categories: Standoff Tags: Game Development

In order to capture all of the capabilities outlined in my design, I began development with a ‘Piece’ class to handle ownership of all the relevant spatial and qualitative traits. This class is responsible for executing all actions available to pieces, and provides a layer of protection for critical data about the game’s state - piece’s are only accessed or altered through the available public methods.

The Standoff project was my first foray into memory management on such a large scale, so I wanted to use it as an opportunity to experiment with different techniques for encapsulating the lifetime of an object. I struggled for a regrettably long time with misplaced attempts to represent the pieces as a vector of pointers, but these proved futile in the long run. I settled on using STL’s smart pointers, introduced in C++11. Although this option is syntactically quite elegant, it’s inefficiency makes it less than ideal for use in game applications. Luckily, Standoff is lightweight enough that performance maximization was unnecessary; in the future I would return to this problem for an exercise in measuring and improving my game’s boot time. [1]

One of the perks of the piece-centric approach is the ability to get by without any concept of a ‘Player’. Almost all data about the game is described by two vectors containing all of the pieces - one for each player. There is no need for a concept of a persistent user either; each ‘Player’ exists only as a collection of associated pieces. In the networked version of the game, a particular game client is tied to a set of pieces based on the order in which it enters the game, with the game creator always controlling the collection labelled as “Player 1”. As a bonus, this meant that I could enable online play without having to set up a database to manage users and their data, nor would i have to invest in securing this data from outside attacks.


Move

With the pieces defined and initialized, I began work on algorithms for the fundamental actions available to a player on their turn - move, deploy, rotate, and shootout. As a deployment is just a special case of the move action, I was able to condense these capabilities down into three methods. Of the three, the shootout function is the most notable; both Game_c::move() and rotate() simply serve to validate and store any relevant data about the move, before passing it along to the piece in question.

Rotate

Shootout, on the other hand, is a whole different beast. By far the most complicated subroutine of the game’s core mechanics, I decided to split the functionality of a shootout into two separate functions, Game_c::shootout and detectHit.

void shootout() {
   std::vector<PiecePtr> live_pieces;
   std::vector<PiecePtr> hit_pieces;
   
   std::vector<PiecePtr>::iterator p1_it;
   for (p1_it = mPlayer1Pieces.begin(); p1_it != mPlayer1Pieces.end(); ++p1_it)
   {
      if ((*p1_it)->getPlayState() == Piece_n::LIVE)
      {
         live_pieces.push_back(*p1_it);
      }
   }
   
   // copy construct pointers to Player 2's live pieces...
   
   std::vector<PiecePtr>::iterator it;
   for (it = live_pieces.begin(); it != live_pieces.end(); ++it)
   {
      if ((*it)->getPieceType() != Piece_n::PAWN)
      {
         Piece_n::Direction_e direction = (*it)->getDirection();
         detectHit2(**it, direction, live_pieces);
   
         if ((*it)->getPieceType() == Piece_n::SLINGER)
         {
            Piece_n::Direction_e secondary_direction;
            switch(direction)
            {
               case Piece_n::UP : { secondary_direction = Piece_n::RIGHT; }
               case Piece_n::DOWN : { secondary_direction = Piece_n::LEFT; }
               case Piece_n::LEFT : { secondary_direction = Piece_n::UP; }
               case Piece_n::RIGHT : { secondary_direction = Piece_n::DOWN; }
            }
            detectHit2(**it, secondary_direction, live_pieces);
         } } } }

Since a shootout can be initiated by either player during their turn, the shootout function is necessarily player agnostic. A record of all pieces hit during the process is stored as a vector of pairs of the form ( Player #, Index ), where the index is such that it can be passed to the vector::at function to return the corresponding piece for the corresponding player. However, I still needed an algorithm to realize this formulation as my proof of correctness.

// method declaration
void Game_c::detectHit2(Piece_n::Piece_c& piece, 
                        Piece_n::Direction_e direction,
                        std::vector<PiecePtr>& pieces)

std::vector<PiecePtr>::iterator it;
for (it = pieces.begin(); it != pieces.end(); ++it)
{ 
   switch (direction)
   {
      case Piece_n::UP :
      {
         std::vector<PiecePtr>::iterator it;
         for (it = pieces.begin(); it != pieces.end(); ++it)
         {
            if ((*it)->getPlayState() == Piece_n::LIVE)
            {
               if ((*it)->getPosition().first == piece.getPosition().first &&
                   (*it)->getPosition().second < piece.getPosition().second)
               {
                  if (hit_piece_flag &&
                     (*it)->getPosition().second > hit_piece->getPosition().second) {
                     hit_piece = *it;
                  }
                  else {
                     hit_piece = *it;
                     hit_piece_flag = true;
                  } } } }
         break;
      }
      // cases for all possible piece directions...
   } 
}

if (piece.getTeam() != hit_piece->getTeam())
{
   hit_piece->nextPlayState();
}

To this end, I wrote the detectHit method, a helper for the more generalized shootout process. During each iteration of the shootout method, detectHit is called if and only if the piece is both in play and capable of shooting. This function takes a reference to a piece, a firing direction, and a reference to a list of pieces to check for a collision. Using this information, it draws a collision ray in the desired direction, checking for hits with each of the pieces stored in the list parameter. This list is determined by the shootout function, which passes the piece vector belonging to the shooter’s opponent. To account for the possibility of multiple hits in the same direction, the detectHit routine only returns the index of the nearest hit piece, determined by an absolute value comparison along each piece’s relevant axis. In case the shooting piece is a ‘slinger’ - the type of piece capable of firing in two directions - a second call is made to detectHit, passing the piece’s secondary direction instead. In the final step of the process, the shootout algorithm sets each of the hit pieces to the ‘DEAD’ play state, rendering them invisible to the application for all intensive purposes.

Shootout