Supporting Undo

In puzzle games, it's important to consider alternate solutions. It's especially important in games where some moves cannot be reversed, such as pushing a box into a corner. We'll use the game history we've accumulated to step backward and reverse our most recent move when the user shakes the device.

Getting on with it

Our design says that the user should be able to undo their last move by shaking the device.

Recognizing the Undo requests

Shaking is recognized by looking at the accelerometer events. These events are dispatched only to the global Runtime target; our UI will need to listen to this target, and stop listening when its scene is not active, but ideally we don't want the game scene to know that a submodule needs to be connected to Runtime or disconnected later. Perform the following steps to recognize the Undo requests:

  1. Since the interface for the game scene is generated in the createScene event, the interface can register and unregister itself by listening to the Scene object for the enterScene and exitScene events. We'll add the functions to listen or stop listening to Runtime first, in interface.lua:
    local function engage(self, event)
      Runtime:addEventListener('accelerometer', self)
    end
    local function disengage(self, event)
      Runtime:removeEventListener('accelerometer', self)
    end
    return function(game)
  2. Then we just need to attach the following functions to the interface when it is constructed, and start listening for the right scene events:
      target:addEventListener('tap', self)
      self.enterScene = engage
      game:addEventListener('enterScene', self)
      self.exitScene = disengage
      game:addEventListener('exitScene', self)
      local counter = display.newText(self, "0", 10, 10, native.systemFont, 24)
  3. And finally, since the interface object itself is being registered as the listener for the accelerometer events, it needs an appropriate method:
      game:addEventListener('exitScene', self)
      function self:accelerometer(event)
        if event.isShake then
          self:dispatchEvent{name = 'Move'; action = 'undo'}
        end
      end
      local counter = display.newText(self, "0", 10, 10, native.systemFont, 24)

Fortunately, we don't have to do any complex tracking of accelerometer data to recognize whether the user is shaking the device; Corona uses routines provided by the host OS to determine that for us.

Saving move history and backing moves out

So now the interface can dispatch the undo events for Move when the device is shaken. The Game object is already listening for the Move events on the interface, so we need to add some recognition to the existing routine. Open game.lua and find the scene:Move function.

    if move.action == 'push' and checkWin(self.Map) then
      self:dispatchEvent{name = 'Game'; action = 'stop', state = self.Map, Game = self}
    end
  elseif event.action == 'undo' then
  end
end

The Move function currently only acts when the requested action is an attempt to move. When that's not the case, we can now check whether the Move was an undo request, instead.

The question then becomes how to restore the game back to its state before a given move. Fortunately, each Move event on the game that's either a push or step event contains the positions of the moving elements before the move was completed. So we can actually save these events themselves as a way of tracking the game history (specifically, the push events, since player steps don't meaningfully advance the game). Perform the following steps for saving move history and backing moves out:

  1. Right now, the game scene uses a number to count how many push events have taken place. But since we're going to be saving them in chronological order in an array, we can use the length of that array to know how many moves have taken place instead. Find the scene:enterScene code in game.lua and change the following line:
      self.Map = require "map" (sok.load(event.params.File, event.params.Level))
      self.MoveCount = 0
      self.World:Load(self.Map)

    So that it reads the following:

      self.Map = require "map" (sok.load(event.params.File, event.params.Level))
      self.History = {}
      self.World:Load(self.Map)
  2. We can then match the other code in scene:Move that formerly relied on MoveCount to this new system:
        if move.action == 'push' then
          table.insert(self.History, move)
        end
        move.count = #self.History
        self:dispatchEvent(move)
  3. Now that our pre-existing code is working on the new system, we can use the most recent Move event in the history as a way of resetting the Move action:
      elseif event.action == 'undo' then
        local lastMove = table.remove(self.History)
        if lastMove then
        end
      end

    The if block just ensures that we don't attempt to undo the beginning of the game.

  4. Since we don't save step events in the history, we can't guarantee that the player is still in the position it was in immediately after the move being undone. Although the box pushed will be in the same position as it was in immediately after the move being undone, since this is the most recent push. So we get the player's current position from the map, clear that position, and set the player's position before the move being undone as his/her current position:
        if lastMove then
          local column, row = self.Map:FindPlayer()
          local realm = self.Map.Moving
          realm[row][column] = nil
          realm[lastMove[1].y][lastMove[1].x] = '@'
        end
  5. The local variable realm is just used to shorten our code and eliminate a lot of tedious repeating of table lookups. Then, we can fetch the box that was moved and restore it from the position it was moved to.
          realm[lastMove[1].y][lastMove[1].x] = '@'
          local xFinal, yFinal = lastMove[2].x + lastMove.x, lastMove[2].y + lastMove.y
          realm[lastMove[2].y][lastMove[2].x] = realm[yFinal][xFinal]
          realm[yFinal][xFinal] = nil
        end
  6. Finally, we reload the world to match the current state of the Map entity (this is much easier than trying to regress it) and issue an event with the revised move count to force the interface to update.
          realm[yFinal][xFinal] = nil
          self.World:Load(self.Map)
          self:dispatchEvent{name='Move'; count = #self.History}
        end

At this point, undo should work. You can load the app to your device and test it the real way, or it will simulate device shakes although the simulator doesn't supply most accelerometer input.

What did we do?

We replaced a simple move count with a series of Move actions that we can reverse. We didn't have to generate any new data structures to hold the history as the events we already used to process gameplay have all the needed info!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.162.110